diff --git a/.changeset/tidy-lions-smile.md b/.changeset/tidy-lions-smile.md new file mode 100644 index 0000000..ebc6bd2 --- /dev/null +++ b/.changeset/tidy-lions-smile.md @@ -0,0 +1,32 @@ +--- +'@seamless-auth/react': minor +--- + +Stop painting the disabled submit button as a filled grey primary. + +On the sign-in and MFA screens the submit button was disabled until its field +validated, and while disabled it was filled with `--seamless-disabled` while the +label kept `--seamless-accent-contrast`. Those two colours were chosen in +different places, so no value a themed app could supply worked in both a light +and a dark theme: lighten the fill and the label washed out, darken it and the +label went near-black on dark. The result read as a primary button that had +broken rather than a control waiting on input. + +The disabled state is now the enabled button at reduced opacity. Label and +background stay on the accent pair the app already tuned, so their contrast +cannot invert with the theme, and the control reads as inactive instead of +broken. This matches how the magic-link and passkey screens already draw their +disabled buttons. + +`--seamless-disabled` is no longer read anywhere and has been dropped from the +token table. If you set it, remove it; every other token behaves as before. + +The sign-in screen now also says why the button is refusing, in a live region +below it that reports whether the field is empty, incomplete, or ready. 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. + +Fixing that surfaced a related bug: a valid email typed in registration left the +Login button enabled after switching to sign-in, even with the identifier field +empty, because the submit check fell through to the registration field. Each +mode now checks only its own field. diff --git a/README.md b/README.md index 5121d4c..f30fa89 100644 --- a/README.md +++ b/README.md @@ -971,7 +971,6 @@ There is no provider prop or JavaScript API for this. Setting the variables is t | `--seamless-border` | Input, button, and panel borders | `#4b5563`, `#d1d5db` | | `--seamless-text` | Headings and body text | `white` | | `--seamless-text-muted` | Labels, helper text, secondary copy | `#9ca3af`, `#d1d5db` | -| `--seamless-disabled` | Background of disabled submit buttons | `#9ca3af` | | `--seamless-danger` | Error messages | `#f87171` | | `--seamless-success` | Success messages and the verified check icon | `#34d399` | | `--seamless-warning` | OTP countdown and resend timers | `#facc15` | @@ -990,6 +989,10 @@ There is no provider prop or JavaScript API for this. Setting the variables is t - Two decorative tints are deliberately not tokenised: the pulse ring behind the magic-link mail icon and the disc behind the success check. Both are translucent and sit directly under an icon, so an opaque override would hide the icon it is meant to frame. +- Disabled buttons are not a separate colour. They are the enabled button at reduced opacity, so the + label and its background always come from the same accent pair you set and the contrast between + them cannot invert when the theme changes. `--seamless-disabled` used to set a standalone grey fill + and no longer does anything; remove it from your overrides. - The package ships one palette and no `prefers-color-scheme` rules. If you want the auth UI to follow the system theme, wrap your own overrides in a media query. diff --git a/src/styles/login.module.css b/src/styles/login.module.css index 2bdcd6d..184b18e 100644 --- a/src/styles/login.module.css +++ b/src/styles/login.module.css @@ -67,20 +67,32 @@ color: var(--seamless-accent-contrast, white); padding: 0.5rem; border-radius: 0.375rem; - transition: background-color 0.3s; + transition: + background-color 0.3s, + opacity 0.3s; cursor: pointer; margin-top: 1rem; } -.button:hover { +.button:hover:not(:disabled) { background-color: var(--seamless-accent-hover, #1d4ed8); } +/* Fading the enabled pair keeps the label and its background on the same + accent the app already tuned, so a theme cannot invert one without the + other. A separate disabled fill could not do that. */ .button:disabled { - background-color: var(--seamless-disabled, #9ca3af); + opacity: 0.6; cursor: not-allowed; } +.submitHint { + font-size: 0.75rem; + color: var(--seamless-text-muted, #9ca3af); + margin-top: 0.5rem; + text-align: center; +} + .toggle { margin-top: 1.5rem; width: 100%; diff --git a/src/styles/mfaLogin.module.css b/src/styles/mfaLogin.module.css index b16014c..e2d3d22 100644 --- a/src/styles/mfaLogin.module.css +++ b/src/styles/mfaLogin.module.css @@ -94,11 +94,13 @@ color: var(--seamless-accent-contrast, white); font-weight: 500; cursor: pointer; - transition: background-color 0.2s; + transition: + background-color 0.2s, + opacity 0.2s; } .submit:disabled { - background-color: var(--seamless-disabled, #9ca3af); + opacity: 0.6; cursor: not-allowed; } diff --git a/src/views/Login.tsx b/src/views/Login.tsx index 425fdbe..bcea136 100644 --- a/src/views/Login.tsx +++ b/src/views/Login.tsx @@ -48,8 +48,8 @@ const Login: React.FC = () => { setIdentifier(value); }; - const canSubmit = (): boolean | undefined => { - if (mode === 'login' && identifier) { + const canSubmit = (): boolean => { + if (mode === 'login') { return isValidEmail(identifier) || isValidPhoneNumber(identifier); } @@ -57,6 +57,24 @@ const Login: React.FC = () => { return isValidEmail(email); }; + // A disabled button is skipped by screen readers and explains nothing to + // anyone else, so the reason it is refusing lives in a live region instead. + const submitHint = (): string => { + if (mode === 'login') { + if (!identifier) return 'Enter your email or phone number to continue.'; + + return canSubmit() + ? 'Ready to continue.' + : 'This does not look like a complete email or phone number yet.'; + } + + if (!email) return 'Enter your email address to continue.'; + + return canSubmit() + ? 'Ready to continue.' + : 'This does not look like a complete email address yet.'; + }; + const register = async () => { setFormErrors(''); @@ -237,9 +255,17 @@ const Login: React.FC = () => { {emailError &&
{emailError}
} )} -