Theme & tokens
Every visual value in Joy runs through CSS custom properties. Override a token after loading joy-tokens.css and the whole system — buttons, cards, dock, modal — picks up the change.
Color tokens
Which one to reach for
| Token | Use it for |
|---|---|
--joy-accent | solid buttons, active/selected states, links, focus rings — the one color that means "this is interactive or selected" |
--joy-accent-soft | a light wash behind something accent-colored — chip backgrounds, badge backgrounds, hover fills |
--joy-text | headings and primary body copy |
--joy-text-muted | secondary copy, captions, placeholder-style text, anything that should read as less important |
--joy-text-on-accent | text or icons placed directly on an --joy-accent background, so it stays readable in both themes |
--joy-bg / --joy-surface | page background vs. a card/panel sitting on top of it — keeping them as two tokens is what gives cards their contrast |
Glass tokens
The translucent-blur look (topbar, dock, modal, .joy-glass from Cards & layout) is also token-driven:
| Token | Role |
|---|---|
--joy-glass-bg | default translucent fill |
--joy-glass-bg-strong | more opaque variant, used where legibility matters more than the effect (hovered dock item, modal) |
--joy-glass-border | the hairline border every glass surface gets |
--joy-glass-shadow | the soft drop shadow underneath |
Blur scale
Not tied to any single component — reach for these directly in your own CSS with backdrop-filter: blur(var(--joy-blur-md)) or similar:
| Token | Value |
|---|---|
--joy-blur-sm | 10px |
--joy-blur-md | 16px |
--joy-blur-lg | 24px |
Building your own theme
A theme override is just CSS loaded after joy-tokens.css that redefines some of its variables — no build step, no config file, no fork of the source. Here's a complete example: a purple brand color, plus a matching dark-mode variant:
:root {
/* Light mode — loads after joy-tokens.css, before joy-dark.css */
--joy-accent: #6d28d9;
--joy-accent-hover: #5b21b6;
--joy-accent-glow: rgba(109, 40, 217, 0.35);
--joy-accent-soft: rgba(109, 40, 217, 0.1);
}
/* Dark mode — joy-dark.css only redefines color tokens under :root, with
no extra selector guarding it (see "How the switch works" below), so
your override needs the same specificity: plain :root, placed AFTER
the <link id="joy-dark-theme"> tag in the <head> order. */
:root {
--joy-accent-soft: rgba(109, 40, 217, 0.18);
}Order in the <head> is what decides the outcome, since every rule here is a plain :root selector (same specificity). Load your override stylesheet after both joy-tokens.css and joy-dark.css so it wins regardless of which theme is active.
Dark mode: how the switch actually works
joy-dark.css is a second stylesheet tag, always present in the page but starting disabled. Almost all of it is :root variable redefinitions — the same variable names as joy-tokens.css, different values — plus a small number of component-specific tweaks where a plain token swap wasn't enough (the background blobs, and the dock's selected-item text color). It never touches layout or spacing, so enabling it can't rearrange anything, only recolor it.
<link id="joy-dark-theme" rel="stylesheet" href="https://cdn.vaneltonmedia.com/joy/styles/joy-dark.css" disabled>
Toggling the theme is one line: flip that disabled attribute. JoyTheme.toggle() does exactly this, plus saves the choice:
| Action | How |
|---|---|
| Toggle | window.JoyTheme.toggle() — or click a button with id="joy-theme-toggle", which joy.js wires up on its own |
| Force a theme | window.JoyTheme.apply('dark') or .apply('light') |
| Persistence | saved to localStorage['selectedTheme'], read automatically on the next visit |
| No saved choice | falls back to the OS prefers-color-scheme |
Which tokens actually change between the two themes — everything else (spacing, radius, easing, accent unless you override it) stays identical:
| Token | Light | Dark |
|---|---|---|
--joy-bg | #f0f2f5 | #141417 |
--joy-bg-alt | #eaeef2 | #1a1a1f |
--joy-surface | #ffffff | #1c1c22 |
--joy-text | #14141a | #f5f5f7 |
--joy-text-muted | #55555f | #a8a8b3 |
--joy-glass-bg | rgba(255,255,255,.6) | rgba(28,28,34,.55) |
--joy-glass-bg-strong | rgba(255,255,255,.85) | rgba(28,28,34,.85) |
--joy-glass-border | rgba(255,255,255,.8) | rgba(255,255,255,.08) |
--joy-accent-soft | rgba(205,0,0,.1) | rgba(205,0,0,.18) |
--joy-accent itself doesn't change between themes by default — the same brand red reads fine on both. If your brand color needs to shift for dark mode too (a very light color might not have enough contrast on a dark surface, for instance), redefine it inside your own dark-mode block, the same way joy-dark.css redefines --joy-accent-soft above.
No-flash-of-wrong-theme script — reads the saved choice before first paint and flips the attribute immediately, so there's no visible flicker on load. This goes right after the <link id="joy-dark-theme"> tag:
<script>(function(){var t=localStorage.getItem('selectedTheme')||(matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light');if(t==='dark'){document.getElementById('joy-dark-theme').disabled=false;}})();</script>Spacing scale
| Token | Value |
|---|---|
--joy-space-1 | 4px |
--joy-space-2 | 8px |
--joy-space-3 | 12px |
--joy-space-4 | 16px |
--joy-space-5 | 24px |
--joy-space-6 | 32px |
--joy-space-7 | 48px |
--joy-space-8 | 64px |
Every margin/padding/gap utility in Sizing & spacing and Flexbox & grid maps to one of these — override the scale here and every one of those classes follows.
Border radius
| Token | Value |
|---|---|
--joy-radius-sm | 14px |
--joy-radius-md | 20px |
--joy-radius-lg | 28px |
--joy-radius-xl | 40px |
--joy-radius-pill | 999px |
Easing
| Token | Value | Used for |
|---|---|---|
--joy-ease | cubic-bezier(0.25, 0.8, 0.25, 1) | Default transitions |
--joy-ease-bounce | cubic-bezier(0.175, 0.885, 0.32, 1.275) | Hover/click bounce (buttons, dock) |
Font
Everything uses --joy-font: 'Poppins', sans-serif;. To swap it, override the token and load whichever font you prefer instead of the Google Fonts link (see Getting started). For the full size scale — proportional steps and fixed-pixel options — see Typography.