How to Build a Two-Mode Color System in Figma (Light + Dark, No Crying).
Build a proper Figma Variables color system from scratch — primitives, semantic tokens, and light/dark modes that switch in one click. Real steps, no hand-waving.
By the end of this tutorial, you'll have a two-layer Figma color system — primitives on the bottom, semantic tokens on top — that switches between light mode and dark mode with a single click. Every component in your file will update automatically. No more 'find all instances of this blue and change them one by one.'
It won't be a full enterprise design system. That takes another six hours and a team argument about naming conventions. But you'll have the core mechanic that makes real systems work, and you'll understand why it's built this way — which is half the battle.
What you need: Figma (free tier works — Variables are available to all accounts for personal files), a basic color palette (6–8 hex values is enough to start), and about 20 minutes.

Step 1: Create your Primitives collection
Open the Local Variables panel. On a Mac: right-click the canvas → Edit variables. Or click the grid icon in the right sidebar. Create a new collection. Name it exactly Primitives.
This collection holds your raw colors — the actual hex values. Nothing else. No meaning, no 'button background.' Just the paint.
Common mistake: Don't name your primitives things like 'button-blue' yet. Call them 'blue-500,' 'blue-300,' 'neutral-900.' They're paint swatches. The meaning comes later.
Step 2: Add your primitive colors
Inside Primitives, add your colors. Click + to add each variable, set type to Color. A starter set that works for most UIs: neutral-0 #FFFFFF, neutral-100 #F5F5F5, neutral-600 #737373, neutral-800 #262626, neutral-900 #111111, blue-300 #93C5FD, blue-500 #2563EB, red-500 #EF4444, green-500 #22C55E.
Pro tip: Name variables with a slash to create sub-groups. 'blue/500' and 'blue/300' will nest under a 'blue' folder in the panel. Makes the list navigable when it grows to 30+ colors.
Step 3: Create your Semantic collection
Add a second collection. Name it Semantic. This is where meaning lives.
Every variable in Semantic will point to a Primitive — it won't hold a hex value of its own. Think of it like this: Primitives are your paint. Semantic is the painter's label system — 'this wall gets the blue paint, this one gets white.' When you rebrand and change your blue, you update ONE primitive. Every semantic token that references it updates everywhere, instantly.
That's the whole reason the two-layer system exists.
Step 4: Add modes to your Semantic collection
With the Semantic collection selected, click Add mode in the top right of the panel. Add two modes: name them Light and Dark. Delete the default 'Mode 1.'
Common mistake: Modes live on the Semantic collection, not on Primitives. Your raw blue-500 doesn't change between modes — it's always blue-500. What changes is which primitive your semantic tokens point to per mode.
Step 5: Build your first semantic token
In Semantic, add a new variable: background/primary. Set the Light mode value: click the swatch → choose 'Library variable' → pick neutral-0 from Primitives (white). Set the Dark mode value: pick neutral-900 (near-black).
That's it. You just built one token. The background will be white in Light, near-black in Dark. The alias — pointing to a primitive — is what makes the whole file switch.
Pro tip: The slash in 'background/primary' isn't just organization. It tells anyone reading the file what category this token belongs to (background) and what role it plays (primary). Category + role + hierarchy is the naming system real design teams use.

Step 6: Build out the rest
Do the same for each type of color your UI needs. Starter tokens: background/primary (Light: neutral-0, Dark: neutral-900), background/secondary (Light: neutral-100, Dark: neutral-800), text/primary (Light: neutral-900, Dark: neutral-0), text/secondary (Light: neutral-600, Dark: neutral-400), brand/primary (Light: blue-500, Dark: blue-300), feedback/error (Light: red-500, Dark: red-400), feedback/success (Light: green-500, Dark: green-400).
Seven tokens will get you through a real screen. Don't add tokens until a component needs them. Premature token creation is where systems go to die.
Step 7: Apply semantic variables to a component
Create a button. Select the background fill. In the fill panel, click the variable icon (it looks like a hexagonal node) → choose brand/primary from your Semantic collection. For label text, apply text/primary or a dedicated text/on-brand token if your button text is always white regardless of mode.
The rule: Apply Semantic variables to components. Never Primitive variables. If you wire up blue-500 directly to a button fill, dark mode won't work — you've skipped the layer that does the switching.
Step 8: Test the switch
Select your frame. In the right sidebar under Local variables, you'll see a mode switcher. Flip it from Light to Dark. Every element using a semantic variable will update. Anything that didn't change either has a raw hex fill (no variable), a Primitive variable applied directly (wrong layer), or it's on a different frame that hasn't had its mode set yet.
Step 9: Handle colors that don't change
Some colors stay constant across modes. White text on a colored button is still white in dark mode. Two ways to handle this: create text/on-brand that maps to neutral-0 in BOTH modes (cleaner for documentation), or apply the primitive neutral-0 directly in this specific case (faster, acceptable when intentional). The rule isn't 'never use primitives on components' — it's 'be intentional about why.'

Step 10: Prepare for handoff
Before sharing, rename any vague tokens. background/primary is fine. color1 is not. Then right-click the Semantic collection → Export as JSON. That file is what your developer drops into their tokens plugin (Tokens Studio, Style Dictionary, whatever they use). Going solo? You're already done. The system works. Add a 'Tokens' page to your file with swatches of every semantic color in both modes — future-you will thank present-you.
If your components aren't switching
Check three things in order. First: does the element have a variable applied, or a raw hex? Look for the hexagonal icon in the fill panel — if it's absent, there's no variable. Second: is it a Semantic variable or a Primitive? If the variable name reads blue-500 instead of brand/primary, you're on the wrong layer. Third: is the frame's mode actually set? Modes are per-frame, not global — you can have a Light frame and a Dark frame side by side in the same file. Which, by the way, is genuinely useful when a client says they 'haven't decided' which mode they want. Show both. Let them decide by looking at the thing.
Next up: applying this same two-layer logic to typography — using Number variables so your type scale adjusts automatically when you switch between desktop and mobile. Same idea, different data type.
Done reading? There’s more where this came from.
