Browse docs
Browse docs
Tailwind is excellent. So excellent, in fact, that the temptation is to stop there: install it, scaffold a Button with bg-blue-600 hover:bg-blue-700 active:bg-blue-800 disabled:opacity-50 ..., and ship.
The problem is the second Button. And the third. And the third Checkbox, and the second TextField with the third validation flavor, and the fourth dark-mode-aware toggle that nobody quite remembers how to make accessible.
This page is about that drift — what it costs, and what Dashforge changes.
Tailwind is, by design, utility-first. It atomizes styling. What it deliberately does not do is compose those atoms into components. Composition is left to you, the consumer.
That's a feature, until you have a team. Then you discover:
Button in your codebase looks slightly different.disabled:cursor-not-allowed.dark: variants".<button role="switch"> that someone bolted together with a translate-x span — sometimes accessible, often not.The styles are easy to write and easy to drift. Tailwind gave you the alphabet; you still have to write the dictionary, in every project, every time.
Consider a tiny WorkspaceSettings card: a header, two toggles, a checkbox, and a save button. Built straight on Tailwind, it's ~50 lines of markup — most of it spent hand-rolling the switch (state, aria-checked, the translate-x thumb animation), repeating utility chains for the two toggle rows, and re-asserting hover/focus/disabled/dark-mode styles on every button.
Built with @dashforge/tw, the same card is ~19 lines. Not because we hid the markup — because the markup we hid was boilerplate you'd write the exact same way every time.
<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" />State lives inside the component. aria-checked is wired. Hover, focus, disabled, dark mode — pre-set. The thumb animation is one Radix primitive deep. You write what changes (label, default state); the rest is invariant.
The argument isn't "less code is better". The argument is less code you'd have written identically anyway. That delta buys you:
cursor-not-allowed".@dashforge/tw-tokens), components consume them via a Tailwind preset, the runtime mirrors them as CSS variables. Designers change a color in one place; the whole app follows.| Concern | Tailwind only | @dashforge/tw |
|---|---|---|
| Component API | Utility classes, every file | Typed props (color, variant, size) |
| State for stateful atoms (Switch, etc.) | Your useState | Internal, controllable when you want |
| Dark mode | dark: variants per file | CSS vars flipped by the provider, components ignore the question |
| Tokens | Your tailwind.config.js extends | Typed token surface + preset + reactive runtime |
| A11y | Hand-rolled, easy to forget | Pre-wired (Radix, focus-visible, aria-*) |
| RBAC | DIY (effects + props) | access={...} prop on every interactive component |
| Variant catalogue | Conventions in your team | tailwind-variants slots + compound variants |
| Refactor blast radius | Wide (markup repeats) | Narrow (API stable) |
You still write Tailwind. The components ARE Tailwind underneath — they emit utility classes, you can className-override them, your IntelliSense still works, your purge step still purges. We bring the layer that lives between "raw utilities" and "a polished app"; we don't replace either end.
You also don't lose escape hatches. Every component accepts className (merged via tailwind-merge) and slotProps (per-slot override). When you need to break out of the API, you break out cleanly.
If the argument lands, head to Installation. Three commands, two minutes, one working component on screen.
If you're still on the fence, the Components section has live demos of what we ship today — Tier-1 atoms only for now; the catalogue grows on a published roadmap.