Skip to content

Control boundaries drawn with --border are 1.2-1.5:1, under SC 1.4.11's 3:1 #16

Description

@malinfossum

.input, .textarea and .select draw their edge with 1px solid var(--border) on a --surface-2 fill. Measured against the page ground, that boundary is between 1.22:1 and 1.50:1 in every theme and palette — WCAG 2.2 SC 1.4.11 Non-text Contrast (AA) asks for 3:1 on the visual information needed to identify a user interface component.

No border token in the system clears the bar, so this cannot be fixed by swapping one token for another.

Measured

Theme Pair Ratio
Base dark --border #232c35 vs page #000000 1.48
Base dark --border vs field --surface-2 #0a0d10 1.38
Base dark --border-strong #313c48 vs page 1.87
data-palette="wend" dark --border vs page #0d1117 1.34
data-palette="wend" dark --border vs field #151b24 1.22
data-palette="wend" dark --border-strong vs page 1.69
data-palette="wend" dark field #151b24 vs page #0d1117 1.09
Light --border #d8dee4 vs page #f6f8fa 1.27
Light --border vs field #ffffff 1.36
Light --border-strong #c5ced6 vs page 1.50

For reference, --text-faint #87919c reaches 5.91:1 against the page and 5.40:1 against the field — but it is a text token, and reaching for it here would be a component borrowing a role that is not its own.

How it surfaced

In Wend (wendhq/wend#54). Its auth inputs had never carried a class at all, so they had been rendering with the browser's default control styling — a 2px #858585 inset border at 5.13:1. Adding class="input" fixed a target-size gap (32px → 51.59px) and, in the same stroke, dropped the boundary contrast from 5.13:1 to 1.34:1.

SC 1.4.11 exempts a control "where the appearance is determined by the user agent and not modified by the author". An unstyled input has that exemption. .input does not. So the design system is what decides whether a consumer conforms here, which is why this belongs upstream rather than in any one app's stylesheet.

The same argument applies to any interactive component whose only boundary is --border — the ghost and secondary button variants are the other case. Decorative edges on .card, .alert, .table and .toast are outside the SC and should not change.

Suggested direction

A dedicated control-boundary token — something like --control-border — set per theme and per palette to clear 3:1 against both the page ground and the component's own fill, with .input / .textarea / .select and the bordered button variants pointing at it. --border keeps its current value for the decorative cases, so nothing that is already correct moves.

Precedent: the 42px→44px control floor went the same route and reached consumers as bundle 2.0.2.

Decided

Malin, 2026-08-21: fix it here and sync out; no consumer-local override. Keeping consumers in sync is the point of the shared bundle, and an override in one app would leave every other consumer wrong while giving that app a second source of truth for a number the system already owns.

Tracked on the consumer side in wendhq/wenddocs/backlog.md, "A styled auth input has a 1.34:1 boundary against the page".

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions