Dashforge-UI
tailwind
Docs
New1.1.1 · Sprint 4.4 — nine new presentational primitives

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/tw
Works with:
React 19React Hook FormTailwind v3TypeScript
MIT license@dashforge/tw on npmGitHub stars

From zero to first component

One import. One prop. Done.

App.tsx
import { Button } from '@dashforge/tw';

<Button color="primary">Save changes</Button>
previewLive
Built on tech you trust
React 19TypeScriptTailwind v3Radix UIReact Hook FormZodVite
Why we rewrote Tailwind

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.

1

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.

2

Forms

Forms that validate themselves.

3

Reactions

Fields that orchestrate themselves.

4

Permissions

UI that knows who's looking.

Switch.tsx
Live

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

Validation, touched-state, error gating — all from the Zod schema. Zero hand-wired.
Schema → form, no glue

Checkout

Async-safe · stale dropped
id: 'load-cities',
watch: ['country'],
run: async (ctx) => {
const rid = ctx.beginAsync('cities');
const data = await fetch(...);
if (!ctx.isLatest('cities', rid)) return;
ctx.setRuntime('city', { options: data });
}

Click 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

The system, painted

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.

workspace.acme.com

indigo workspace

Acme Corp

KS

Active customers

+12 this week
Gl
Globex
Active
In
Initech
Trial
Um
Umbrella
Active
MRR$48.2k+18% on May. Recurring & stable.
>patchTheme({ 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.

Component composition

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
Utility-first
45 LoC
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 Cancel
38 </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 changes
41 </button>
42 </div>
43 </div>
44 );
45 }
47%markup
Props-first
24 LoC
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

The form layer

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
ViewingAfter·DashForm + typed fields, rules as a propSignIn.tsx
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>
  );
}
Beyond static fields

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.

ViewingAfter·visibleWhen + access props, zero useEffectSupportForm.tsx
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.

Stop hand-rolling. Start composing.

Dashforge-UI tailwind

Typed, token-driven components for Tailwind apps. RHF-wired, RBAC-aware, semver-stable. Built and maintained from Italy.