Skip to content

Color: relax text floors as a priced policy, not an absolute bar #603

Description

@mehdibha

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:10bg-muted … text-fg-muted
  • www/src/registry/ui/segmented-control/styles.ts:8bg-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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: colorsColor engine and color tokens (@dotui/colors)enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions