Skip to content

fix(styles): stop painting the disabled submit as a filled grey primary - #141

Merged
Bccorb merged 1 commit into
mainfrom
fix/disabled-submit-reads-as-broken-primary
Sep 4, 2026
Merged

fix(styles): stop painting the disabled submit as a filled grey primary#141
Bccorb merged 1 commit into
mainfrom
fix/disabled-submit-reads-as-broken-primary

Conversation

@Bccorb

@Bccorb Bccorb commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Closes #131

The problem

--seamless-disabled filled the disabled submit button while its label kept
--seamless-accent-contrast. The two colours are chosen in different places, so
the fill could not be tuned for one theme without breaking the other, and the
result read as a primary button that had broken rather than a control waiting on
input.

Measured against the fixed #9ca3af fill that shipped:

dark theme (defaults) light theme (dark label)
before 2.54:1 7.46:1
after (opacity 0.6) 3.38:1 3.93:1

The dark theme was the failing side, and the only lever available to a consuming
app (darkening --seamless-disabled) would have broken the light theme's dark
label. Both figures sit below 4.5:1, which is expected: WCAG 1.4.3 exempts
inactive controls. The point is that after the change they are set by the same
pair, so one cannot invert without the other.

The change

Fade the enabled pair rather than swapping in a separate fill. The label and its
background stay on the accent the app already tuned. This matches how the
magic-link and passkey screens already draw their disabled buttons, and 0.6 is
the value registerPasskey.module.css uses. I compared 0.5, 0.6 and 0.7 on both
themes: 0.7 is close to indistinguishable from enabled, 0.5 washes out the light
theme's label.

.button:hover became .button:hover:not(:disabled). Without that, a disabled
button would still pick up the hover fill, which the old ordering happened to
suppress.

--seamless-disabled is no longer read anywhere. It is out of the token table,
with a note telling anyone who set it to remove it. That is why the changeset is
minor rather than patch.

Saying why it is disabled

The sign-in screen now reports whether the field is empty, incomplete, or ready,
in a role="status" live region below the button and referenced by
aria-describedby. A disabled button is not focusable and is passed over by
screen readers, so the refusal was previously silent for the people least able to
guess the reason.

A related bug this surfaced

canSubmit() was gated on mode === 'login' && identifier, so an empty
identifier fell through to isValidEmail(email), the registration field. Typing a
valid email in registration and switching to sign-in left the Login button enabled
with the identifier box empty. No honest hint could be written on top of that, so
each mode now checks only its own field. Covered by a new test.

Notes for review

  • mfaLogin.module.css is not imported anywhere today, consistent with there
    being no bundled MFA route, so it does not reach dist. It carried the same
    rule and is fixed here so the bug does not ride along into that screen when it
    lands, but that half of the diff cannot be verified by rendering.
  • dist/index.js was checked after building: it carries opacity:.6 and no
    occurrence of seamless-disabled.

Checks

npm run typecheck, npm run lint, npm run format:check, npm run build and
npm run check-npm-build all pass. 321 tests across 32 suites pass, including
four new ones covering the hint states and the mode-switch bug.

The submit button on the sign-in screen was filled with --seamless-disabled
while its label kept --seamless-accent-contrast. Those come from different
places, so no value a themed app could supply worked in both a light and a
dark theme, and the result read as a primary button that had broken rather
than a control waiting on input.

Fade the enabled pair instead. Label and background stay on the accent the
app already tuned, so their contrast cannot invert with the theme. This is
how the magic-link and passkey screens already draw disabled buttons.
--seamless-disabled is no longer read anywhere and leaves the token table.

Say why the button is refusing, too, in a live region below it. A disabled
button is not focusable and is passed over by screen readers, so the refusal
was silent for the people least able to guess the reason.

That surfaced a related bug: a valid email typed in registration left the
Login button enabled after switching to sign-in with the identifier field
empty, because the submit check fell through to the registration field.
Each mode now checks only its own field.

Closes #131
@Bccorb
Bccorb force-pushed the fix/disabled-submit-reads-as-broken-primary branch from f64609b to 066c3a4 Compare September 4, 2026 01:14
@Bccorb
Bccorb merged commit eb10397 into main Sep 4, 2026
3 checks passed
@Bccorb
Bccorb deleted the fix/disabled-submit-reads-as-broken-primary branch September 4, 2026 01:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The disabled submit button reads as a broken primary button

1 participant