Browse docs
Browse docs
Released 2026-05-19 · Sprint 3 release
Five new Tier-4 components close the gap with what MUI's Dashforge line offers at the overlay/disclosure level. The lib goes from 24 → 29 components total. Plus three internal-quality deliverables: a formal parity audit between TW and MUI lines, a customization playbook for the docs lab, and the first performance baseline.
Strictly additive minor bump. No breaking change on the 24
existing components. Drop-in upgrade from 0.3.0-beta.
| Package | What changed |
|---|---|
@dashforge/tw | +5 Tier-4 components (Dialog · Tabs · Tooltip · Popover · Accordion) + 5 new Radix runtime deps + PARITY.md + PERFORMANCE.md + customization docs page. 634/634 unit tests passing (37 files). Bundle: 312 KB raw / 68.85 KB gzipped. |
Unchanged (independent versioning):
| Package | Version (unchanged) | Why |
|---|---|---|
@dashforge/tw-theme | 0.1.0-beta | No source change — peer dep stays at ^0.1.0-beta. |
@dashforge/tw-tokens | 0.1.0-beta | No source change — peer dep stays at ^0.1.0-beta. |
Bridge (forms, rbac, ui-core) | 0.2.3-beta | Shared with MUI side; no source change. |
MUI side (ui, theme-mui, theme-core, tokens) | 0.2.3-beta | Separate ecosystem; untouched. |
Dialog-backed)import { Dialog, Button } from '@dashforge/tw';
const [open, setOpen] = useState(false);
<Button onClick={() => setOpen(true)}>Open dialog</Button>
<Dialog
open={open}
onOpenChange={setOpen}
title="Confirm action"
description="This will delete the selected items."
size="md"
>
<p>Body content here.</p>
<div className="flex justify-end gap-2 pt-4">
<Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
<Button color="danger" onClick={handleDelete}>Delete</Button>
</div>
</Dialog>sm (max-w-sm) · md (max-w-md, default) · lg (max-w-2xl).<body>,
aria-modal="true".aria-labelledby / aria-describedby.document.body — no z-index conflicts with
the rest of your tree.disableBackdropClose to
override). Esc closes by default (disableEscapeClose to
override).× close button on by default (showCloseButton={false}
to hide).motion-reduce:.Tabs-backed)import { Tabs } from '@dashforge/tw';
<Tabs
items={[
{ value: 'overview', label: 'Overview', content: <OverviewPanel /> },
{ value: 'details', label: 'Details', content: <DetailsPanel /> },
{ value: 'history', label: 'History', content: <HistoryPanel /> },
]}
defaultValue="overview"
variant="underline" // or "pill"
orientation="horizontal" // or "vertical"
/>underline (default — line + active border) ·
pill (rounded background, used for filter-like UIs).horizontal (default) · vertical (sidebar
layout).aria-orientation reflects the
variant, role="tablist" / role="tab" / role="tabpanel"
emitted by Radix.value) or uncontrolled (defaultValue) modes.
Default value defaults to the first item if neither is set.disabled: true skips that tab in keyboard nav.Tooltip-backed)import { Tooltip, Button } from '@dashforge/tw';
<Tooltip content="Delete this item" side="top">
<Button variant="ghost" aria-label="Delete">
<TrashIcon />
</Button>
</Tooltip>asChild slot — no DOM
doubling.role="tooltip", aria-describedby
wired automatically by Radix.side (top/right/bottom/left), align
(start/center/end), delayDuration (default 200ms),
sideOffset, hideArrow.open + onOpenChange for tutorials /
programmatic reveals.Popover-backed)import { Popover, Button } from '@dashforge/tw';
<Popover
content={
<div className="flex flex-col gap-2">
<h3 className="text-sm font-semibold">Quick actions</h3>
<Button variant="ghost" size="sm">Duplicate</Button>
<Button variant="ghost" size="sm">Archive</Button>
<Button variant="ghost" size="sm" color="danger">Delete</Button>
</div>
}
side="bottom"
width="240px"
>
<Button>Actions</Button>
</Popover><Tooltip> semantics ("read-only hint") would be wrong.side / align / sideOffset props identical to Tooltip's
surface.showArrow={true} (off by default — most
popover designs prefer a clean edge).width prop accepts any CSS length.Accordion-backed)import { Accordion } from '@dashforge/tw';
<Accordion
type="single"
collapsible
defaultValue="q-1"
items={[
{ value: 'q-1', header: 'What is Dashforge?', content: <p>…</p> },
{ value: 'q-2', header: 'How does it integrate with my forms?', content: <p>…</p> },
{ value: 'q-3', header: 'What about RBAC?', content: <p>…</p> },
]}
/>
// Multi-mode (multiple sections open simultaneously):
<Accordion
type="multiple"
defaultValue={['filters', 'sort']}
items={[…]}
/>type="single" (default — at most one section
open; collapsible: true lets the user close the open section
by clicking it again) · type="multiple" (any number open).aria-expanded on triggers,
role="region" on panels.data-state=open selector — pure
Tailwind, no React state, no JS animation.disabled: true skips it in keyboard nav.motion-reduce:.New file at the package root, mirrors the format of A11Y.md.
Tabulates per-component parity between the two Dashforge UI lines
across four axes: signature, behavior, variant axis, bridge
contract. Covers the 10 bridge-integrated components (Button,
TextField, Checkbox, Switch, RadioGroup, Textarea, NumberField,
OTPField, Autocomplete, DateTimePicker).
Findings summary:
✗ rows — no unintended drift discovered.✓ overall (DateTimePicker) — the only component
byte-identical at the prop level across both lines. Both lines
chose native HTML5 inputs, leaving very little surface to
diverge on.⚠ overall — all intentional, all documented:
onCheckedChange(checked), onValueChange(value)); MUI
inherits HTML onChange(event, value).solid / outline / soft / ghost,
sm / md / lg); MUI uses Material vocabulary
(contained / outlined / text, small / medium /
large). 1:1 mappable.multiple / freeSolo / loadOptions on
Autocomplete (TW is feature-ahead); slotProps.prefix /
suffix on TextField (new in 0.3.0-beta); showStepper on
NumberField; resize variant on Textarea.optionsFromFieldData on Autocomplete
(form runtime data integration — F6+ work to port to TW).Motivation: the audit exists for Dashforge's internal maintainability (preventing silent drift across two lines we maintain in parallel) and as the foundation for Sprint 5 starter kits (two parallel kits require parity at the call-site level). It is NOT a customer migration document — the "switch story" MUI↔TW was scrapped as a non-existent use case (no consumer in real life switches across UI ecosystems).
The doc closes with a maintenance pact: every release touching a bridge-integrated component on either line MUST re-run the audit.
/docs/guides/customization.mdx — escape hatch playbookNew page in the docs lab. Three sections:
sx vs slotProps decision tree + 4 canonical examples:
outer wrapper styling, slot styling, conflict resolution
(testing-backed), combining both.@dashforge/tw-theme.PhoneInput
tutorial showing how to hook into useDashFieldMeta +
useAccessState + tv() to build form-integrated custom
fields that get RBAC + visibility + form-closure error gating
for free.The page opens with a "Why two override paths?" section that
explicitly answers the question a TW-native developer asks first:
"why is the prop called sx and not className?". Answer: sx
carries a tailwind-merge semantics guarantee that bare
className would not signal.
New file at the package root. Documents:
pnpm up @dashforge/tw@^0.4.0-betaNo code changes required. Existing usages keep working
byte-identical. Adopt the new Tier-4 components by importing them
from @dashforge/tw:
import {
Dialog,
Tabs,
Tooltip,
Popover,
Accordion,
} from '@dashforge/tw';| Compatibility axis | Pre-0.4.0 | Post-0.4.0 |
|---|---|---|
| Public API surface | 24 components + foundation + bridge hooks | + 5 components (Dialog · Tabs · Tooltip · Popover · Accordion) + their *Props / *SlotProps types + *Variants recipes (additive — opt-in, zero impact on existing usages) |
| Peer deps | react ^18 || ^19, tw-theme workspace, tw-tokens workspace | unchanged |
| Bridge deps | forms / rbac / ui-core workspace:* | unchanged |
| New runtime deps | — | @radix-ui/react-dialog ^1.1.0 · @radix-ui/react-tabs ^1.1.0 · @radix-ui/react-tooltip ^1.1.0 · @radix-ui/react-popover ^1.1.0 · @radix-ui/react-accordion ^1.2.0 |
| Breaking changes | — | Zero. The sx vs className design discussion concluded with "keep both, document only" — no rename, no API surface change. |
| Bundle size | 272 KB raw / ~60 KB gzipped | 312 KB raw / 68.85 KB gzipped (+40 KB raw / +8.85 KB gz; within the projected 14% budget for 5 Radix-backed components) |
| Tests passing | 592/592 (32 files) | 634/634 (37 files) — +42 new tests for the 5 Tier-4 components |
| Sprint | Release | Theme |
|---|---|---|
| Sprint 4 | @dashforge/[email protected] | Tier-5: DataGrid + Table + Pagination + Skeleton — closing parity with MUI's grid story. |
| Sprint 5 | @dashforge/[email protected] + starter kits v1 | Two separate starter kit repos (dashforge-starter-mui + dashforge-starter-tw). NOT a unified starter — the architectural decision is "pick one ecosystem per app". |
| Sprint 6 | @dashforge/[email protected] → 1.0.0 | Final A11Y audit (axe / lighthouse CI), bundle size lockdown, beta freeze 4 weeks, then cut 1.0.0. |