Tailwind, reinvented.Components, not utilities.
You're already shipping with Tailwind. Stop hand-rolling the same buttons, inputs, and modals every project. Dashforge gives you 24 typed components and 8 layout primitives that emit Tailwind utility classes you can still override. Same Tailwind you write today — half the markup, RHF-wired, RBAC-aware.
Ship in 24 hours. Not 24 sprints.
pnpm add @dashforge/twWorkspace settings
Control access and notifications.
From zero to first component
One import. One prop. Done.
import { Button } from '@dashforge/tw';
<Button color="primary">Save changes</Button>Tailwind shows. Dashforge orchestrates.
Tailwind hands you classes. State, forms, permissions, reactive fields — the four things every dashboard needs and Tailwind doesn't ship — Dashforge orchestrates. Scroll through them.
Interactive state
State that wires itself.
Switches, checkboxes, menus, dialogs — Tailwind shows them. You wire aria-checked, focus, open / close, keyboard, dark mode. Dashforge does that.
Forms
Forms that validate themselves.
Reactions
Fields that orchestrate themselves.
Permissions
UI that knows who's looking.
Workspace settings
Public access
Anyone with the link can view
Email notifications
Weekly activity digest
Tailwind raw
~45 LoC
role · aria · focus · state · dark
Dashforge
3 lines
label + prop. Done.
New customer
Invalid email format
Checkout
Async-safe · stale droppedClick two countries fast. Only the last response lands — `isLatest` drops stale ones.
Customer detail
Name
Globex Industries
Plan
Pro · $99/mo
Try the role picker. Same JSX, three roles, one prop.
<Switch checked onCheckedChange={setX} />Problem 1 of 4 · Interactive state
Brand it in five seconds.
One token, the whole product re-skins. No find-and-replace across the codebase, no per-component overrides. Pick an accent, watch the dashboard recolour live.
indigo workspace
Acme Corp
Active customers
+12 this weekpatchTheme({ color: { primary: indigoPalette } })Tokens drive every surface. Buttons, chips, badges, gradients, focus rings, the stat panel — all carry the accent. Swap the preset, ship a different brand. Same code.
Style-first, or props-first?
Same WorkspaceSettings card. Same three useState hooks on both sides. Same pixels in light and dark. Read both at once — the highlighted rows show where the hand-rolled switch markup collapses into a single Switch / Checkbox component.
0
LoC, utilities
0
LoC, props
−0%
Markup removed
- Same useState — no hand-rolled toggle markup
- Hover · focus · disabled · dark mode pre-wired
- A11y (role · aria-checked · keyboard) baked in
- Variants via props, not utility lookup
1 import { useState } from 'react';2 3 export function WorkspaceSettings() {4 const [pub, setPub] = useState(true);5 const [emails, setEmails] = useState(false);6 const [twofa, setTwofa] = useState(false);7 8 return (9 <div className="rounded-xl border border-neutral-200 bg-neutral-50 p-6 shadow-sm">10 <h3 className="text-lg font-semibold text-neutral-900">Workspace settings</h3>11 <p className="mb-5 text-sm text-neutral-600">Control access and notifications.</p>12 13 <div className="flex items-center justify-between border-b border-neutral-100 py-3">14 <p className="text-sm font-medium">Public access</p>15− <button role="switch" aria-checked={pub} onClick={() => setPub(!pub)}16− className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${pub ? 'bg-primary-600' : 'bg-neutral-200'}`}>17− <span className={`inline-block h-5 w-5 transform rounded-full bg-neutral-50 shadow-md transition-transform ${pub ? 'translate-x-5' : 'translate-x-0.5'}`} />18 </button>19 </div>20 21 <div className="flex items-center justify-between border-b border-neutral-100 py-3">22 <p className="text-sm font-medium">Email notifications</p>23− <button role="switch" aria-checked={emails} onClick={() => setEmails(!emails)}24− className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${emails ? 'bg-primary-600' : 'bg-neutral-200'}`}>25− <span className={`inline-block h-5 w-5 transform rounded-full bg-neutral-50 shadow-md transition-transform ${emails ? 'translate-x-5' : 'translate-x-0.5'}`} />26 </button>27 </div>28 29− <label className="flex items-center gap-2 py-3 cursor-pointer">30− <input type="checkbox" checked={twofa} onChange={(e) => setTwofa(e.target.checked)}31− className="h-4 w-4 rounded border-neutral-300 text-primary-600 focus:ring-2 focus:ring-primary-500" />32 <span className="text-sm">Require 2-factor authentication</span>33 </label>34 35 <div className="mt-4 flex justify-end gap-2">36− <button className="px-4 py-2 rounded-md text-sm font-medium text-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-800">37 Cancel38 </button>39− <button className="px-4 py-2 rounded-md bg-primary-600 text-sm font-semibold text-white hover:bg-primary-700 active:bg-primary-800">40 Save changes41 </button>42 </div>43 </div>44 );45 }1 import { useState } from 'react';2 import { Box, Typography, Stack, Switch, Checkbox, Button } from '@dashforge/tw';3 4 export function WorkspaceSettings() {5 const [pub, setPub] = useState(true);6 const [emails, setEmails] = useState(false);7 const [twofa, setTwofa] = useState(false);8 9 return (10 <Box variant="outlined" elevation={1} rounded="xl" p={6}>11 <Typography variant="h6">Workspace settings</Typography>12 <Typography variant="body2" color="muted" gutterBottom>Control access and notifications.</Typography>13 14+ <Switch name="pub" label="Public access" checked={pub} onCheckedChange={setPub} />15+ <Switch name="emails" label="Email notifications" checked={emails} onCheckedChange={setEmails} />16+ <Checkbox name="twofa" label="Require 2-factor authentication" checked={twofa} onCheckedChange={setTwofa} />17 18 <Stack direction="row" justify="end" gap={2}>19 <Button variant="ghost">Cancel</Button>20 <Button color="primary">Save changes</Button>21 </Stack>22 </Box>23 );24 }hand-rolled·collapsed into a single prop·same pixels, same behaviour, same dark mode
Thirty-six lines of useState. Or thirteen.
Validation, touched state, error gating, submit wiring — handled. Pass the rules as a prop and move on. Same imports, same submit, same fields — only the glue is gone.
36
LoC, before
13
LoC, after
−64%
Code removed
- React Hook Form wired through DashForm
- Validation rules as a prop, not a callback
- Touched + dirty + error gating, automatic
- A11y attributes (aria-invalid, aria-describedby) for free
import { DashForm } from '@dashforge/forms';
import { TextField, Button } from '@dashforge/tw';
import { signIn } from './api';
export function SignIn() {
return (
<DashForm onSubmit={signIn}>
<TextField name="email" type="email" required rules={{ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ }} />
<TextField name="password" type="password" required rules={{ minLength: 8 }} />
<Button type="submit">Sign in</Button>
</DashForm>
);
}Thirty-six lines of useEffect. Or twenty-two.
Built on Dashforge's reactive engine — visibleWhen and access props handle conditional rendering and RBAC at the prop level. No scattered effects, no prop drilling, no manual permission gating in every component.
import { DashForm } from '@dashforge/forms';
import { RadioGroup, TextField, Button } from '@dashforge/tw';
export function SupportForm() {
return (
<DashForm>
<RadioGroup name="category" options={CATEGORIES} required />
<TextField
name="bugDetails"
label="Steps to reproduce"
visibleWhen={(engine) => engine.getNode('category')?.value === 'bug'}
required
/>
<Button
color="danger"
access={{ resource: 'invoice', action: 'delete', onUnauthorized: 'hide' }}
>
Delete invoice
</Button>
</DashForm>
);
}visibleWhen
Reactive predicate — the field unmounts/remounts when the dependency changes. Zero useEffect.
access
One prop wires the field to your RBAC layer. Hide, disable, or read-only by role.
rules
React Hook Form validation, declarative. Pass min/max/pattern/custom validators inline.