Skip to content

feat(styles): make the built-in auth UI themeable with CSS custom properties - #129

Merged
Bccorb merged 1 commit into
mainfrom
feat/themeable-ui
Aug 18, 2026
Merged

feat(styles): make the built-in auth UI themeable with CSS custom properties#129
Bccorb merged 1 commit into
mainfrom
feat/themeable-ui

Conversation

@Bccorb

@Bccorb Bccorb commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Problem

src/styles/*.css carried 118 hardcoded colour literals (42 distinct) and zero custom properties, 28 of them blue. An application embedding <AuthRoutes /> got a blue login form regardless of its own brand palette. This surfaced while building an app generator on top of seamless-templates: the generated app had a deep-spruce and orange palette everywhere except the login form, which stayed blue and looked pasted in.

Change

Every colour declaration now reads from a --seamless-* custom property with the previous literal as its fallback. Consumers theme the auth UI by setting variables on :root or on any ancestor of AuthRoutes. There is no provider prop and no JavaScript surface.

The literals collapse onto 16 semantic tokens named for their role:

Group Tokens
Accent accent, accent-hover, accent-contrast, accent-soft
Surface surface, surface-raised, surface-hover
Neutral border, text, text-muted, disabled
Status danger, success, warning
Effects overlay, shadow

Near-duplicate shades share a token while each declaration keeps its own original value as the fallback, so one override unifies the role without shifting today's defaults. Greys split by role rather than by value: #4b5563 maps to border where it is a border and to surface-raised where it is an input background.

Three tokens push the count past a tighter 12, and each prevents a regression for consumers who do theme. Without surface-hover the MFA method buttons lose their hover state entirely, since base and hover would both resolve to surface-raised. Without accent-soft links render in the brand accent, which is often unreadable on a dark card. Without disabled a disabled submit button becomes indistinguishable from an active one.

Non-breaking

Stripping the var() wrappers from the new files yields output byte-identical to the previous version across all 10 modules. That simultaneously proves the change is colour only (no layout, spacing, typography, or class changes) and that every fallback is exactly the value it replaced. Applications that set nothing render as before.

The built CSS in dist/index.js matches the previous build modulo var() indirection, with one benign delta: cssnano can no longer collapse the passkey spinner's four border longhands into border-color: #3b82f6 transparent, so it emits the longhands. Same computed result, a few bytes larger.

Deliberately left hardcoded

  • rgba(99, 102, 241, 0.15) (magic-link pulse ring) and rgba(16, 185, 129, 0.15) (success check disc). Both are translucent tints sitting directly under an icon, so an override that drops the alpha would paint an opaque disc over the icon it is meant to frame. The other rgba() values have nothing on top and were safe to tokenise.
  • Three transparent keywords (spinner border sides, secondary button background). Intentionally invisible rather than themeable.

Judgement calls worth a look

  • The passkey registration CTA was green (#059669), the only non-blue primary button. It maps to accent and accent-hover rather than success, so a consumer's brand colour styles it like every other primary action. The default stays green.
  • registerPasskey's .loading and .supported panels are light-on-light islands in an otherwise dark UI. They map to the generic surface and text pair so they join the rest of the theme on override instead of staying light. That drops the green tint on the supported panel for theming consumers. The alternative was two single-use tokens.

Dark mode

There were no prefers-color-scheme blocks anywhere in the package, so there was no existing behaviour to preserve. The README points consumers at a media query if they want the auth UI to follow the system theme.

Docs

A Theming The Built-In UI section in the README covers the :root override example, a wrapper-scoped variant, the full token table with defaults, and notes on the multi-default rows, the --seamless-shadow alpha collapse, the light-surface and white-text gotcha, and the two untokenised tints.

Checks

npm run typecheck, npm run lint, npm run format:check, npm test (290 passed, 32 suites), and npm run build all pass.

…perties

Every colour in the bundled screens now reads from a --seamless-* custom
property with the previous literal as its fallback. Consumers can match the
auth UI to their brand by setting variables on :root or on any ancestor of
AuthRoutes, with no provider prop and no JavaScript surface.

The 118 hardcoded literals collapse onto 16 semantic tokens: four accent
tokens, three surface tokens, four neutral tokens, three status tokens, and
two effect tokens (overlay and shadow). Near-duplicate shades share a token
while each declaration keeps its own original value as the fallback, so a
single override unifies the role without shifting the defaults.

Four values stay hardcoded. The magic-link pulse ring and the success check
disc are translucent tints sitting under an icon, so an override that drops
the alpha would paint an opaque disc over the icon it frames. The three
transparent keywords are intentionally invisible rather than themeable.

This is opt-in and non-breaking. Stripping the var() wrappers from the new
files yields output byte-identical to the previous version, so applications
that set nothing render exactly as before.
@Bccorb
Bccorb merged commit 4ea4fd0 into main Aug 18, 2026
2 of 3 checks passed
@Bccorb
Bccorb deleted the feat/themeable-ui branch August 18, 2026 04:07
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.

1 participant