Browse docs
Browse docs
You've installed and verified the Button paints. This page walks you from there to a real screen — a workspace-settings card with toggles, a checkbox, a save action, theme switching, and the escape hatches you'll need when our defaults don't fit.
<DashforgeTailwindProvider> is the only mandatory ancestor. Mount it once, as high in the tree as practical:
// main.tsx
import { createRoot } from 'react-dom/client';
import { DashforgeTailwindProvider } from '@dashforge/tw-theme';
import { App } from './App';
import './index.css';
createRoot(document.getElementById('root')!).render(
<DashforgeTailwindProvider>
<App />
</DashforgeTailwindProvider>
);That's it for setup. Every @dashforge/tw component below this point reads the reactive theme from context — no per-component prop drilling, no theme-prop-of-the-week.
Here's the WorkspaceSettings card from the landing-page comparison, written end-to-end:
import { Switch, Checkbox, Button } from '@dashforge/tw';
export function WorkspaceSettings() {
return (
<div className="rounded-xl border border-neutral-200 bg-white p-6 shadow-sm dark:border-neutral-700 dark:bg-neutral-900">
<h3 className="text-lg font-semibold">Workspace settings</h3>
<p className="mb-5 text-sm text-neutral-600 dark:text-neutral-400">
Control access and notifications.
</p>
<Switch label="Public access" helper="Anyone with the link can view." defaultChecked />
<Switch label="Email notifications" helper="Weekly activity digest." />
<Checkbox label="Require 2-factor authentication" />
<div className="mt-4 flex justify-end gap-2">
<Button variant="text">Cancel</Button>
<Button color="primary">Save changes</Button>
</div>
</div>
);
}The chrome (<div>, padding, border, header text) is raw Tailwind — we don't ship a <Card> yet, and that's deliberate. The atoms (Switch, Checkbox, Button) absorb the cognitive load. You write what the design system can't infer.
Theme mode is reactive. Read it and flip it with the hooks exported by @dashforge/tw-theme:
import { useDashTWTheme, toggleMode } from '@dashforge/tw-theme';
import { Button } from '@dashforge/tw';
export function ThemeToggle() {
const { mode } = useDashTWTheme();
return (
<Button variant="outlined" onClick={() => toggleMode()}>
{mode === 'dark' ? '☀ Light' : '🌙 Dark'}
</Button>
);
}Behind the scenes: toggleMode() flips the Valtio-backed store, the provider patches the dark class on <html> and re-publishes the CSS variables. Tailwind's dark: variant lights up across your entire tree. No re-render of unrelated components — Valtio only notifies subscribers.
Persisted automatically in localStorage under dashforge-tw-mode.
Three escape hatches, in order of increasing scope:
classNameEvery component merges your className over the variant defaults via tailwind-merge. The last word wins:
<Button color="primary" className="bg-fuchsia-600 hover:bg-fuchsia-700">
Black sheep
</Button>Useful for the rare one-offs where a designer asks for "the campaign pink, just this once".
slotPropsComponents built on multiple slots (label, input, helperText, etc.) accept slotProps to target each slot precisely:
<TextField
label="Email"
slotProps={{
label: { className: 'uppercase tracking-wider text-xs' },
helperText: { className: 'italic text-warning-600' },
}}
/>Catalogue of slots per component lives in each component's reference page.
For "every primary in our app is teal, forever", patch the theme:
import { patchTheme } from '@dashforge/tw-theme';
import { defaultTWThemeLight } from '@dashforge/tw-tokens';
patchTheme({
colors: {
primary: {
...defaultTWThemeLight.colors.primary,
500: '#0d9488', // teal-600
600: '#0f766e',
700: '#115e59',
},
},
});CSS variables update; every bg-primary-500, text-primary-600, <Button color="primary"> follows immediately. No rebuild.
If you're shipping forms, install @dashforge/forms and wrap with <DashForm> — every @dashforge/tw input registers automatically via the bridge:
import { DashForm } from '@dashforge/forms';
import { TextField, Switch, Button } from '@dashforge/tw';
export function SignIn() {
return (
<DashForm onSubmit={(data) => console.log(data)}>
<TextField name="email" type="email" required />
<TextField name="password" type="password" required rules={{ minLength: 8 }} />
<Switch name="remember" label="Remember me" />
<Button type="submit" color="primary">Sign in</Button>
</DashForm>
);
}The validation rules, the touched/dirty/error gating, the submit pipeline — handled. See Form System → Quick Start for the full tour.