Browse docs
Browse docs
Released 2026-05-18 · Sprint 2 release
Nine fixes across seven components plus one new public API
(TextField inline adornments). Two of the fixes close WCAG gaps
identified in the 0.2.1 A11Y audit. End-to-end validation in the
dash consumer app caught the highest-severity functional bug —
Autocomplete loadOptions mode failing to commit selections —
which was invisible to both unit tests and the docs lab because
both used static option arrays.
Minor bump because of the additive TextField slotProps.prefix / suffix API. All other changes are patch-level in isolation;
existing TextField usages keep working byte-identical. Drop-in
upgrade from 0.2.1-beta.
| Package | What changed |
|---|---|
@dashforge/tw | 9 fixes across Autocomplete (2), LeftNav, Breadcrumbs / TopBar, AppShell (focus trap + motion-reduce), Snackbar (motion-reduce), Switch (motion-reduce), Checkbox (indeterminate dash glyph) + new TextField slotProps.prefix/suffix API. 591/592 functional tests pass (1 perf-timing flake unrelated). |
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. |
slotProps.prefix / slotProps.suffixThe biggest user-visible addition. Closes a long-standing doc/lib
drift discovered during the Sprint 1 P3 docs work: the
text-field.mdx page already documented inline prefix/suffix
adornments but the actual lib never exposed them. Now it does:
<TextField
name="price"
type="number"
slotProps={{
prefix: { children: '#x27; },
suffix: { children: 'USD' },
}}
/>Both slots accept { children?: ReactNode; className?: string }.
The slot wrapper carries aria-hidden="true" + pointer-events-none
— the adornment is purely visual decoration; the <input> remains
the labeled control; clicking the adornment doesn't steal focus
from the input (focus-within on the wrapper handles ring styling).
Three things you can do with this you couldn't before:
$ / € / £, suffix
USD / kg / %.children is unconstrained.children).Empty configs add zero layout cost — the slot only renders when
children !== undefined. Existing <TextField> usages keep
working byte-identical (no rendering change, no className shift).
Sighted-keyboard users opening the drawer on a narrow viewport were previously able to Tab past the drawer and land on hidden- but-still-tabbable elements in the page beneath. Per WAI-ARIA APG dialog pattern, modal overlays must trap Tab/Shift+Tab so focus cycles inside.
The implementation is hand-rolled (no focus-trap-react dep,
~50 LOC):
document.activeElement (the
hamburger button that opened the drawer).requestAnimationFrame → move focus to first focusable
element inside the drawer.Tab / Shift+Tab:
wraps focus from last → first (and first → last for Shift+Tab).Plus role="dialog" + aria-modal="true" applied to the drawer
<aside> when open so screen readers announce it as a modal
overlay rather than just an aside. Dropped on close so the
closed-state aria-hidden=true continues to work.
Verified end-to-end in the dash consumer (/test-layout): all
five check points pass (closed-state ARIA, open-state ARIA,
focus moves in on open, Tab wraps, Esc closes + restores focus
to trigger).
prefers-reduced-motion gates (WCAG 2.3.3)WCAG 2.3.3 ("Animation from Interactions") asks that motion-
sensitive users — vestibular disorders, motion sickness — have
the option to suppress animations triggered by their own
interactions. Tailwind ships a motion-reduce: variant that
maps to @media (prefers-reduced-motion: reduce). We applied
it to six substantial motions:
| Component | Motion | Gate |
|---|---|---|
| Switch | thumb slide on toggle | motion-reduce:transition-none |
| AppShell | drawer slide-in | motion-reduce:transition-none motion-reduce:duration-0 |
| AppShell | backdrop opacity fade | same |
| Snackbar | item slide-in / out | same |
| Autocomplete | chevron flip on open | motion-reduce:[&>svg]:transition-none |
| LeftNav | rail-mode width transition | motion-reduce:transition-none motion-reduce:duration-0 |
The animated end-state still applies; only the smooth tween
between states is suppressed. Color fades (transition-colors
on hover states across TextField / Autocomplete / RadioGroup / …)
are NOT gated — out of WCAG 2.3.3 scope (the spec targets
translate / rotate / major state changes, not micro fades).
Pre-0.3.0, the Indicator rendered <CheckIcon /> for BOTH the
checked and indeterminate Radix states (regression introduced
in 0.2.1 when we dropped forceMount to fix the indicator mount
bug). Now renders a dedicated <DashIcon /> (horizontal stroke)
when indeterminate, <CheckIcon /> when fully checked.
Implementation is pure CSS — no React state needed. Both icons
render inside the Indicator; a group class on the Indicator lets
the children target the parent's data-state attribute via
Tailwind's group-data-[state=…]:hidden selectors. Radix's
data-state remains the single source of truth.
<RadixCheckbox.Indicator className={cn(v.indicator(), 'group')}>
<CheckIcon className="…group-data-[state=indeterminate]:hidden" />
<DashIcon className="…group-data-[state=checked]:hidden" />
</RadixCheckbox.Indicator>loadOptions mode now commits selectionsSeverity: high — affected every consumer using async option loading.
Symptom: type a query in an <Autocomplete loadOptions={…}>,
results appear, click one. The popover closes correctly. But the
input keeps showing your search query instead of the selected
option's label, AND the bridge value isn't updated to the
selected option's value.
Root cause: commitSelection looked up the picked option's
label in the static options prop (which is [] when
loadOptions is configured) instead of the effective option
pool (asyncOptions when the fetch resolved). With no match in
the static array, the label lookup returned undefined and
setInputValue was never called → input stuck at the query
string.
Fix: lookup in the effective pool —
loadOptions && asyncOptions !== null ? asyncOptions : options —
and add asyncOptions / loadOptions to the useCallback
dependency list so the closure receives the latest async state.
Verified end-to-end in dash (/test-tw → "User search (async
loader)"): type "al" → wait for results → click "Alice Cooper"
→ input now reads "Alice Cooper" (pre-fix: it stayed at "al").
Why this was invisible to CI: the unit test suite uses static
options, the docs lab demo uses static options. Only a real
consumer wiring loadOptions to a real API call exposed the
bug. That validation gap is exactly the value of the Sprint 2
P1 consumer validation pass.
The chip ×, clear ×, and dropdown caret ▾ were rendered as
Unicode glyphs that came out as chunky font characters
inconsistent with the rest of the design system. Replaced with
two inline SVG components (CloseIcon + ChevronDownIcon,
mirroring the existing CheckIcon pattern from Checkbox).
width="1em" height="1em" so they scale with parent font-size;
stroke uses currentColor so they inherit text-* Tailwind
utilities. Zero icon-library dependency added.
Bonus: the dropdown chevron now flips 180° on popover open via
[&[aria-expanded=true]>svg]:rotate-180 — pure CSS, no React
state. Animation is gated on prefers-reduced-motion: reduce
(see WCAG section above).
Tailwind's preflight removes the default <a> underline globally
across the page. But environments that disable preflight to
avoid resetting other UI ecosystems (e.g. our docs lab, where
the Tailwind section coexists with MUI in the same page tree)
get the underline back — <a> reverts to browser defaults.
Defensive fix: explicit no-underline hover:no-underline on both
the LeftNav itemLink slot and the Breadcrumbs link slot.
Idempotent in environments where preflight already removes the
underline; necessary in environments where it doesn't.
Same fix covers TopBar transitively — TopBar typically renders
Breadcrumbs in its center slot, so fixing the Breadcrumbs link
slot fixes TopBar's underlined links too.
No code changes required. Drop-in upgrade:
pnpm up @dashforge/tw@^0.3.0-betaThese render slightly differently from 0.2.1-beta even without
any code change on your side:
| Component | Observable change |
|---|---|
| Autocomplete | Chip remove / clear / caret render as crisp SVG instead of Unicode glyphs. Chevron flips 180° on open. |
| Checkbox | Indeterminate state now shows a dash glyph instead of a check (the dash was always the intent — the check was a regression in 0.2.1). |
| LeftNav links | No longer underlined in preflight-off environments. |
| Breadcrumbs + TopBar links | Same as LeftNav. |
| AppShell mobile drawer | Now has a focus trap; role="dialog" + aria-modal="true" while open. Keyboard users will notice Tab cycling within the drawer. |
| Switch / drawer / snackbar / chevron animations | Respect prefers-reduced-motion: reduce — animation tweens suppressed for users with the OS preference set. |
If your tests assert that a Checkbox in indeterminate state
contains the check-mark SVG path (d="M3 8.5l3 3 7-7"), that
assertion needs to flip to the dash path (d="M3 8h10").
Similarly, tests that assert specific Autocomplete icon glyphs
(× / ▾) need to look for the new SVG markup. Otherwise no
test changes required.
| Compatibility axis | Pre-0.3.0 | Post-0.3.0 |
|---|---|---|
| Public API surface | unchanged from 0.2.1 | + TextField slotProps.prefix / slotProps.suffix (additive — opt-in via slotProps, 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 |
| Visual changes that consumers may notice without code changes | — | See "Observable behavior changes" above |