Priority: P1 — blocks #484. Found comparing text-fg-muted against shadcn (2026-08-04).
dotUI currently cannot reproduce shadcn's muted text, and won't be able to at any setting of the #464 text axis. The engine's text bars are absolute, and shadcn sits below them.
The gap
BARS.text900 = { wcag: 4.5, lc: 60 }, enforced by solveText against the worst of steps 25/50/100. Measured on shadcn's own surfaces:
|
on --background |
on --muted |
shadcn light --muted-foreground |
4.73 |
4.34 — below AA |
shadcn dark --muted-foreground |
Lc 51.1 |
Lc 48.1 — below the Lc 60 bar |
Both fail. The lightest tone our bars permit in light mode is oklch 0.5375; shadcn is 0.556. Blocked by 0.019 of lightness.
Per CLAUDE.md's north star — "if a user can't reproduce the look of a Material-, Geist-, or Linear-style system, an axis is missing" — this is a real gap, and shadcn is the most-copied system in the space.
Why the bar is not paranoid
Worth stating plainly, because the instinct is to just drop step 100 from the bar set. Two components put text-fg-muted directly on bg-muted, same element:
www/src/registry/ui/kbd/styles.ts:10 — bg-muted … text-fg-muted
www/src/registry/ui/segmented-control/styles.ts:8 — bg-muted p-[3px] text-fg-muted (root, rest-state labels)
neutral-900 → --color-fg-muted, neutral-100 → --color-muted. The bar is measuring a pairing that genuinely ships. Cost of each tone, on the Kbd label:
|
Kbd label on bg-muted |
| today (L* 42) |
5.31 |
| the #464 default bump (L* 46) |
4.50 — exactly AA |
| shadcn's tone |
4.17 — below |
So relaxing has a nameable price: Kbd and SegmentedControl labels drop below AA.
Proposal
Extend the guaranteePolicy that already exists rather than inventing a mechanism. Today relaxed "prices border-floor misses as warnings" — do the same for text floors.
- Default stays
strict. Flexible ≠ lax by default; relaxed remains opt-in, exactly as for borders.
relaxed lets solveText honor a target past its bar, and reports the miss instead of clamping.
- Warnings must name the pairing, not the rung. "neutral-900 misses its bar" is unactionable and gets dismissed. "Kbd label 4.17:1 on
bg-muted — below AA" gets fixed. The engine knows rungs; only the semantic layer knows Kbd puts those two together.
The real work: a rung → token → component map
Requirement 3 is the substance here. That mapping doesn't exist today — --color-fg-muted alone has 609 call sites across 229 files, so the chain has to be derived, not hand-written.
It's the same map the Ladder inspector needs (linked below). Build once, use twice:
- warnings that name the affected component
- the inspector's "Feeds" column
Preset leak
If a shadcn-fidelity preset ships relaxed, every user who picks it silently inherits sub-AA muted text without ever making that choice. The policy has to be visible on the preset itself, not only in the contrast panel. Needs a decision as part of #484.
Links
Priority: P1 — blocks #484. Found comparing
text-fg-mutedagainst shadcn (2026-08-04).dotUI currently cannot reproduce shadcn's muted text, and won't be able to at any setting of the #464 text axis. The engine's text bars are absolute, and shadcn sits below them.
The gap
BARS.text900 = { wcag: 4.5, lc: 60 }, enforced bysolveTextagainst the worst of steps 25/50/100. Measured on shadcn's own surfaces:--background--muted--muted-foreground--muted-foregroundBoth fail. The lightest tone our bars permit in light mode is
oklch 0.5375; shadcn is0.556. Blocked by 0.019 of lightness.Per CLAUDE.md's north star — "if a user can't reproduce the look of a Material-, Geist-, or Linear-style system, an axis is missing" — this is a real gap, and shadcn is the most-copied system in the space.
Why the bar is not paranoid
Worth stating plainly, because the instinct is to just drop step 100 from the bar set. Two components put
text-fg-muteddirectly onbg-muted, same element:www/src/registry/ui/kbd/styles.ts:10—bg-muted … text-fg-mutedwww/src/registry/ui/segmented-control/styles.ts:8—bg-muted p-[3px] text-fg-muted(root, rest-state labels)neutral-900→--color-fg-muted,neutral-100→--color-muted. The bar is measuring a pairing that genuinely ships. Cost of each tone, on the Kbd label:bg-mutedSo relaxing has a nameable price: Kbd and SegmentedControl labels drop below AA.
Proposal
Extend the
guaranteePolicythat already exists rather than inventing a mechanism. Todayrelaxed"prices border-floor misses as warnings" — do the same for text floors.strict. Flexible ≠ lax by default;relaxedremains opt-in, exactly as for borders.relaxedletssolveTexthonor a target past its bar, and reports the miss instead of clamping.bg-muted— below AA" gets fixed. The engine knows rungs; only the semantic layer knows Kbd puts those two together.The real work: a rung → token → component map
Requirement 3 is the substance here. That mapping doesn't exist today —
--color-fg-mutedalone has 609 call sites across 229 files, so the chain has to be derived, not hand-written.It's the same map the Ladder inspector needs (linked below). Build once, use twice:
Preset leak
If a shadcn-fidelity preset ships
relaxed, every user who picks it silently inherits sub-AA muted text without ever making that choice. The policy has to be visible on the preset itself, not only in the contrast panel. Needs a decision as part of #484.Links