Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-2495Done

A disabled Input's opacity-50 composites its affix text below AA — no ink choice can fix it, because opacity halves whatever you pick

Surfaced by MOTIR-2482 — the first sweep ever to load /settings/organization with axe. Filed rather than absorbed: it reports under color-contrast, but its CAUSE is not an ink choice, so it is not in the --el-text-* arm that card owned.

Repo: motir-core. One PR.

The measurement

tests/e2e/shell-a11y-wide.spec.ts sweeps /settings/organization with WCAG 2.1 A+AA and zero rule exclusions. axe reports color-contrast (serious) on one node:

.text-(--el-icon-field) > .text-(--el-text-secondary)

That is the motir.co/ prefix on the organization-URL field — app/(authed)/settings/organization/_components/OrgGeneralCard.tsx, the addonStart of an Input marked readOnly disabled.

Why no ink fixes it

packages/design-system/src/components/ui/Input.tsx applies disabled && 'cursor-not-allowed opacity-50' to the field WRAPPER. Opacity composites the whole subtree against the page, so it roughly halves the contrast of whatever it contains. The prefix is already --el-text-secondary, the darkest caption ink in the palette and the one that clears AA on every surface (6.80:1 on white) — and at 50% it lands under 4.5. Reaching for a darker token does not help: --el-text at 50% over white composites to roughly mid-grey.

So this is a defect in HOW the design system renders a disabled control, not in which colour a call site picked. It affects every Input that carries an addonStart / addonEnd while disabled, not just this one.

What makes it a design decision, not a patch

1.4.3 exempts disabled controls, so the INTENT here is arguably fine — but the exemption is a human judgement axe cannot make, and it does not reach the affix anyway, which is a <span> beside the input rather than the disabled control itself. Three directions, and the card should pick ONE deliberately:

  1. Give disabled its own tokens instead of opacity — a --el-input-disabled-bg / --el-input-disabled-text pair, so the state is expressed as colour (AA-checkable, palette-swappable) rather than as a filter over everything. Widest blast radius, and the most correct: it is the same argument the colour rule already makes about never inventing a value outside the token layer.
  2. Keep opacity for the control, exempt the affix — the affix is not the disabled thing; render it outside the opacity subtree.
  3. This field is not disabled, it is READ-ONLY — the org URL is a value to read and copy, and a disabled input is unfocusable, so today a keyboard user cannot even select it. Making it readOnly without disabled fixes the contrast AND the reachability, but leaves the field looking editable unless Input grows a read-only treatment.

Read design/ for the settings area before choosing; option 1 and option 3 both change how a shipped control looks.

Acceptance criteria

  • /settings/organization reports ZERO color-contrast violations under the same WCAG_TAGS and zero rule exclusions the other sweeps use.
  • The fix is in the SHARED Input (or in the disabled-state tokens it reads), not only at the OrgGeneralCard call site — every disabled input with an affix is the same defect, and a call-site patch leaves the rest.
  • The chosen direction is recorded in the component's own doc comment with its reasoning, so the next disabled control does not re-derive it.
  • Whatever renders, the disabled state is still visually distinguishable from the enabled one — clearing the contrast rule by making disabled look enabled trades one defect for another.
  • If the org URL becomes read-only rather than disabled, it is keyboard-focusable and selectable, and its value is still not editable — asserted by a test, not by inspection.
  • The .exclude() this defect forced into tests/e2e/shell-a11y-wide.spec.ts's /settings/organization sweep — and the data-testid handle added to OrgGeneralCard to anchor it, both commented with this card's key — are REMOVED in the same PR, so the route returns to a zero-exclusion sweep.

Context refs

  • packages/design-system/src/components/ui/Input.tsxdisabled && 'cursor-not-allowed opacity-50' (line ~66) and the two aria-hidden affix slots (lines ~69 / ~87).
  • app/(authed)/settings/organization/_components/OrgGeneralCard.tsx — the readOnly disabled URL field and its addonStart.
  • packages/design-system/theme.css — the --el-input-* tokens, where a disabled pair would live.
  • tests/e2e/shell-a11y-wide.spec.ts — the sweep that measured it, and the carve-out to remove.