feat(theme): make color.accent optional in defineTheme - #3916
Conversation
ColorScaleConfig required an accent, so a theme could not restyle only the
neutral ramp -- defineTheme({color: {neutralStyle: 'warm'}}) was a type error.
accent is now optional. Without one, the neutral palettes seed from the default
accent's hue and --color-accent, --color-accent-muted and --color-on-accent are
left ungenerated so they fall through to colorDefaults -- the same fall-through
expandColorScale already applies to status, categorical and on-dark tokens.
Note the accent tokens are omitted, NOT defaulted: the scale derived from the
default accent hex is not the same value as the default accent token
(expandColorScale({accent:'#0064E0'}) yields light-dark(#0058D2, #BBC2FF) vs
colorDefaults light-dark(#0064E0, #2694FE)), so seeding the default would
silently re-color every existing theme. A test pins both against drift.
Configs that pass an accent are unchanged, token for token.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR Analysis ReportNo new or modified components detected. Bundle Size Summary
Accessibility AuditStatus: No accessibility violations detected. Generated by PR Enrichment workflow | View full report |
cixzhang
left a comment
There was a problem hiding this comment.
This is a really clean fill for the accent-required gap in #2279 (and the friction called out in the adoption feedback). Making color.accent optional so a theme can restyle just the neutral ramp — neutralStyle, contrast — without being forced to adopt a brand color is exactly the ergonomic win the issue asked for.
What makes this mergeable rather than merely plausible is that you got the backward-compatibility question exactly right: when accent is omitted, the three accent tokens are omitted, not seeded — so they fall through to colorDefaults (the same fall-through the file already uses for status/categorical/on-dark tokens), and every theme that does pass an accent stays byte-identical, token for token. The subtle-but-critical detail — that deriving a scale from #0064E0 produces a different accent than the default token holds, so "just default the seed" would silently re-color existing themes — is called out and pinned by a drift-guard test. The suite covers omit-vs-seed, empty config, the accent: '' nullish-vs-truthy edge, runtime resolution through useTheme() in both modes, and an explicit-accent override. Nicely scoped, too: it deliberately stays out of the tuple/per-scheme accent and inversion territory.
Verified locally on a fresh worktree: the expandColorScale suite (17 tests) and tsc both pass, and the accent-present branch is unchanged from main. Thanks for the thorough tests and the clear writeup.
What this does
You can now build a theme that only restyles the greys — without having to pick a brand colour you don't want.
Why
defineThemeinsisted on anaccentcolour. So if all you wanted was warmer greys, or higher contrast, you still had to supply an accent — and whatever you picked would silently become your theme's brand colour.This is the maintainer's own request in #2279. It implements only suggested fix #1 from that issue.
What changed
color.accentis now optional.neutralStyle,contrastall work) — the greys borrow their hue from the default accent.--color-accent,--color-accent-muted,--color-on-accent) are simply not generated, so they fall back to the default values. This is the same fall-through the file already uses for status, categorical and on-dark tokens.One thing worth a close look 👀
The obvious-looking implementation would be "if there's no accent, just default it to the standard accent hex." That is wrong, and it would quietly re-colour every existing theme.
The accent colour you get by deriving a scale from
#0064E0is not the same value as the default accent token:expandColorScale({accent: '#0064E0'})['--color-accent']light-dark(#0058D2, #BBC2FF)colorDefaults['--color-accent']light-dark(#0064E0, #2694FE)So the accent tokens are omitted, not defaulted — omitting them is what preserves the existing default. There's a test pinning both values, so if anyone later "simplifies" this into the seeded version, it fails loudly instead of silently changing everyone's brand colour.
Scope
Only suggested fix #1. Deliberately not implementing #2 (tuple/per-scheme accents) or #3 — those touch accent-picking semantics and belong with the in-flight Color Studio work.
Checks
useTheme()in light and dark; an empty config; an empty-string accent (found and fixed a real bug here — the seed used a nullish check while the token output used a truthy one, soaccent: ''seeded from a malformed value and dropped the accent tokens); a neutral-only theme with a hand-picked accent override; and a drift guard tying the seed to the default token