Themes & modes
Dark mode. A compact layout. High contrast. Every product ends up with a few switches — and modes let you build each one once, without ever duplicating your token set.
Think of your phone's Settings#
Your phone has an Appearance setting with two options — Light and Dark — and a separate Text Size setting with its own options. You pick one option per setting, and they never interfere: changing text size doesn't change your colours.
Particles works exactly the same way:
| On your phone | In Particles | Example |
|---|---|---|
| A setting (Appearance) | A mode group | Theme · Size · Contrast |
| An option (Dark) | A mode | dark · compact · high |
| Your current choices | The active modes | dark + compact |
Every project starts with one group, called Theme — that's where light and dark live. You can add more groups whenever your product needs another independent switch.
One pick per group. The picks combine on their own — there is no third switch for "dark and compact".
A mode stores only what changes#
Your project's base tokens are the complete set — what developers get by default. A mode is a small overlay on top: it lists only the tokens that differ when it's active. A dark mode might override 15 colour tokens (backgrounds, text, borders) and nothing else — every spacing, radius, and font value simply shines through from the base.
That's why modes are cheap to maintain: when you redesign a button's padding, you change it once in the base, and every mode picks it up automatically.
Creating a mode#
In the token editor, open the mode dropdown at the top and click Add mode. A two-step wizard asks exactly two questions:
| Step | Question | What you do |
|---|---|---|
| 1 | Which group does this mode belong to? | Pick Theme, another existing group — or type a new group name (that alone creates the group). |
| 2 | What is the mode called? | Name it (dark, compact…) and optionally pick a mode in the same group to extend. |
There is no separate "create a group" screen. A group exists the moment a mode names it, and disappears with its last mode — nothing extra to manage.
Never name a combination#
Without groups, teams end up encoding combinations into names: dark, compact-dark, high-contrast-compact-dark… Every new switch doubles the list, and each of those needs its own maintenance.
With groups, a combination is never a thing you build — it's just the current position of two switches. Two groups with two modes each give you four looks from four small overlays; adding a third group adds a couple of overlays, not a rewrite:
Theme: ○ light ● dark Size: ○ comfortable ● compact what you see = dark + compact ← composed live, never built or named
Rule of thumb: if you're tempted to put a dash in a mode name to mean "and also" — that's two groups.
Keep each switch in its lane#
The whole system stays predictable because of one discipline: each group owns its own kind of token. Theme changes colours. Size changes spacing, radii, and type sizes. Because the two switches touch different tokens, they can never disagree — flip dark on and off all day, your layout never moves.
The rare value that depends on both — say, text that needs a heavier weight on dark backgrounds — isn't a conflict either: point the component token at a Theme-owned token, and the Theme switch carries the change.
Extending a mode#
A mode can extend another mode in the same group. "Brand A Dark" can extend "dark" and override only the accent colour — every other dark value is inherited. The value a token shows resolves top-down:
| Wins | Source | Example |
|---|---|---|
| 1st | Override in the active mode | brand-a-dark sets color/primary |
| 2nd | Inherited from the extended mode | dark sets color/background |
| 3rd | The base token | base sets everything else |
Editing tokens while a mode is active#
With a mode selected in the editor, saving a token offers two choices:
| Action | What happens |
|---|---|
| Save in [mode name] | Adds an override to this mode only. The base token is untouched. |
| Save in base | Edits the base value — every mode that does not override it follows along. |
Tokens overridden by the active mode show an orange badge; values inherited from an extended mode show a gray badge; everything else is the base value. To send a token back to its base value, click the reset button next to its override.
Live preview#
The preview panel always reflects the currently selected mode — switch in the dropdown and watch the components restyle instantly, no rebuild needed.
Modules ship their mode groups#
In multi-level setups (Foundation → Brand → Product, a Business plan capability), any module can own modes and every product that links it inherits them. The usual division: Foundation ships the modes its own primitives vary by — a surface ramp that flips in dark mode — and Brand ships the appearance modes that carry its identity, so light and dark travel with the brand. Structural groups like Size usually belong to each product, because density is a product decision. See Token architecture for the layering model.
Modes compose in the same order as tokens — Foundation, then Brand, then Product — so when two layers define a mode of the same name, the higher layer wins the tokens they both touch, and your product's own overrides win last.
Choosing which modes you inherit
Linking a module gives you all of its modes by default. To take only some, open Project → Token modules, expand Inherited themes on that module and switch to Choose themes. Two ways to pin, and the difference is what happens when the module publishes a new mode later:
| Pin | You get | When the module adds a mode |
|---|---|---|
| An entire mode group | Every mode in that group | It arrives automatically |
| Individual modes by name | Exactly the ones you picked | Nothing changes until you pick it |
A mode you don't inherit is gone completely: it never appears in your switcher, and it contributes nothing when your tokens resolve. Excluding a module's modes does not affect its tokens — you keep the whole base layer either way.
Products consume a module at a pinned release — upstream mode edits never change your product until you deliberately accept the update in Project → Token modules.
The big picture#
Putting it all together — where values come from, who owns each switch, and what actually ships:
What developers get#
Each mode ships as its own small CSS block, keyed by its group — so the browser composes the active combination by itself:
<html data-theme="dark" data-size="compact">
[data-theme="dark"] { …colour overrides }
[data-size="compact"] { …structure overrides }| Goal | Command |
|---|---|
| Sync base tokens | particles token-studio sync |
| Generate one mode as CSS | particles theme generate dark --out src/themes/dark.css |
| Generate every mode at once | particles theme generate-all --out-dir src/themes |
| Export one exact combination | particles token-studio export --modes theme:dark,size:compact |
Activating a mode in the product is one attribute on any container element — no separate stylesheet builds, and no dark-compact.css anywhere.
Modes in every export format#
A plain export (no --modes) carries every mode the branch defines, in whatever shape the target language can actually switch on. Pass --modes instead and the selected combination is baked into the base output with no mode blocks on top — that is the one-exact-combination case above.
| Format | How modes arrive |
|---|---|
| tailwind-v4, css | [data-axis="name"] blocks after the base block |
| scss | $modes map — Sass has no runtime cascade, so it ships as data |
| json | { tokens, modes: [{ axis, name, values }] } (a bare array when there are no modes) |
| ts, js | a `modes` export alongside `theme` |
| dtcg | $extensions → "com.particlesui.modes" |
| style-dictionary | $modes |
| flutter | AppTokens.modes — Map<String, Map<String, String>> |
In CSS and Tailwind a mode block redefines only the variables it directly overrides; semantics, component tokens and composition utilities all reference those variables through var(), so they follow automatically. Version-pinned exports (--version) read a release snapshot and carry base values only.