---
title: Dark mode with Figma variables, not duplicate frames
description: Dark mode in Figma without a second set of frames: semantic variables with a Light and a Dark mode, one frame per screen, and the checks to run before handoff.
author: Bob Poláček
topic: Guide
published: 2026-09-24
updated: 2026-09-26
source: https://www.appetiteui.com/blog/dark-mode-figma-variables
---

# Dark mode with Figma variables, not duplicate frames

Dark mode in Figma without a second set of frames: semantic variables with a Light and a Dark mode, one frame per screen, and the checks to run before handoff.

By Bob Poláček, updated September 26, 2026.

To do dark mode in Figma without duplicate frames, put every color on a semantic variable, give its collection two modes, Light and Dark, and point each mode at a color from your palette. Components bind to the semantic variables and a frame picks its mode, so one frame renders both themes, and a color you change afterwards changes in both.

Appetite UI is a Figma UI kit for iOS and Android apps, built like a design system. One token source drives Apple and Material components in light and dark mode, and exports to SwiftUI, Jetpack Compose, Flutter and CSS. Its dark mode is built the way this article describes: the examples are its own tokens, with the values the [color docs](https://www.appetiteui.com/docs/colors) print, and the [tokens page](https://www.appetiteui.com/tokens) switches the same chain between Light and Dark on a phone screen.

**In short**

- A duplicate frame doubles every later edit. A variable mode keeps one frame and stores the dark value on the same variable.
- Variable modes are a paid Figma feature. Figma lists them for Education, Professional, Organization and Enterprise plans.
- A semantic token changes which primitive it points at in each mode. In Appetite UI, `text/strong-950` points at `colors/gray/950` in Light and at `colors/gray/0` in Dark.
- The dark value is a separate decision. Appetite UI points `brand/base` at `colors/blue/500` in Light and `colors/blue/400` in Dark, which lifts its contrast on the near-black ground, #0E0E0F, from 3.68:1 to 5.13:1.
- A mode only reaches what is bound to a variable. Raw fills, shadows and pictures are where dark mode breaks, so check them before handoff.

## A duplicate frame doubles every change you make afterwards.

The quickest way to a dark screen in Figma is to duplicate the light one and recolor it. It works for one screen on one day. From then on every edit happens twice: a new row in a settings list, a renamed label, a button that moves eight points. The two copies drift the first time someone forgets one of them, and after a month nobody can say which copy is right.

A Mode variant on the components moves the same problem one level down. Every component set carries a Light and a Dark version of each variant, the variant count of the set doubles, and every instance gets one more property to set by hand. Appetite UI carried a Mode variant on its iOS sets until release 2.4 removed it. Since then one switch drives dark mode across the whole file, the iOS mirrors included ([changelog](https://www.appetiteui.com/changelog)).

I kept that variant longer than I should have, because one extra property looked harmless. What it really did was put the dark decision on every instance, in every screen, instead of in one place. Taking it out is what made dark mode a property of the frame.

## A variable mode stores the dark value under the same name.

In Figma a collection of variables can have more than one mode, and the variables panel shows each mode as a column. A variable holds one value per mode. A frame or a section picks its mode with Apply variable mode, in the Appearance section of the right sidebar, a page picks one in the Page section, and everything inside takes it on. Figma's help describes the default: objects are set to Auto, which means "they take on the mode of their parent container".

That inheritance is what replaces the duplicate. Set the screen frame to Dark and every nested instance, text layer and fill bound to the collection resolves its Dark value. Set it back to Light and nothing else in the frame has moved.

**Modes need a paid Figma plan:**
Figma's help lists variable modes for "Education, Professional, Organization, and Enterprise plans". Its plans page gives the limits: up to 10 modes per collection on Professional, up to 20 on Organization, and unlimited modes with extended collections on Enterprise. Light and Dark fit every one of them.

![The Color Tokens collection in the Figma variables panel with Name, Light and Dark columns: the fg and text roles strong-950, sub-700, soft-500, disabled-400 and white-0, each pointing at one colors/gray step in Light and another in Dark](https://www.appetiteui.com/images/blog/dark-mode-figma-variables--01.webp)

*Color Tokens: one row per role and one column per mode, each cell pointing at a gray step of the palette.*

## Each mode points at a primitive from the same palette.

A semantic token holds no dark color of its own. It holds one pointer per mode, and each pointer ends on a primitive of the palette. In Appetite UI the primitives sit in the Foundations collection, which has one mode, and the semantic roles sit in Color Tokens, which has two. The color docs put the rule in one sentence: "A role does not change its value in Dark, it changes which primitive it points at."

**Semantic tokens and the primitive each mode points at**

| Token | Light points at | Dark points at |
| --- | --- | --- |
| `text/strong-950` | `colors/gray/950` | `colors/gray/0` |
| `brand/base` | `colors/blue/500`, #155DFC | `colors/blue/400`, #2B7FFF |

`text/strong-950` inverts on its own, from the darkest gray to white, so no component that uses it needs a dark variant. Color Tokens holds 161 variables on those two modes.

Two habits keep the palette the one place a color is defined. Point a Dark value at a primitive that already exists: a hex typed into the Dark column is a color that no edit of the ramp will reach, and the color docs mark the few roles that hold one as direct, so they can be found. And when a value must not switch, point the role at the same primitive in both modes, which the docs mark as a fixed value.

## The dark value is its own decision, and contrast shows why.

Mirroring the ramp for Dark, 950 for 50 and 900 for 100, is a common shortcut, and the brand blue shows where it fails: 500 is the middle of the ramp, so a mirror leaves it where it is. On white, `colors/blue/500` (#155DFC) measures 5.25:1 and clears the 4.5:1 minimum Apple asks for in all appearances. On a near-black ground, `colors/gray/950` (#0E0E0F), the same blue drops to 3.68:1. One step lighter, `colors/blue/400` (#2B7FFF) measures 5.13:1 on that ground, and 3.76:1 on white.

**WCAG 2 contrast of the two brand values on the two grounds**

| brand/base | On white | On near black |
| --- | --- | --- |
| Light, #155DFC | 5.25:1 | 3.68:1 |
| Dark, #2B7FFF | 3.76:1 | 5.13:1 |

Ratios computed from the hex values with the WCAG 2 formula on September 24, 2026. WCAG 2.2 asks for at least 4.5:1 for text and 3:1 for large text; Apple asks to strive for 7:1 for custom colors in small text.

Each value passes on its own ground and fails on the other. That is the whole argument for pointing `brand/base` at a different primitive in Dark: the second value is chosen for the dark ground, once, in the variable, for every screen that uses the token.

## Tinted surfaces turn translucent in Dark.

A mode can change more than the step on a ramp. It can change the kind of primitive a token points at. In Light, the light-200 and lighter-50 steps of a status family point at solid ramp steps, 200 and 50. In Dark they point at the alpha-24 and alpha-16 steps of the same hue ([color docs](https://www.appetiteui.com/docs/colors)).

The result shows on a soft error banner. In Light it is a pale solid red. In Dark it stays a tint of red over the dark ground, where a solid pastel would sit on the screen as a bright slab. The same pattern runs through every status family in the file, from error and success to verified and faded.

![A Payment failed error banner and a Backup complete success banner in Light and in Dark with their tokens: state/error/lighter-50 resolves to colors/red/50 in Light and colors/alpha/red/alpha-16 in Dark, state/success/lighter-50 to colors/green/50 and colors/alpha/green/alpha-16](https://www.appetiteui.com/images/blog/dark-mode-figma-variables--02.webp)

*The same two tokens in both modes: solid steps in Light, alpha steps in Dark.*

## The iOS and Material mirrors read the same dark set.

A file that draws both platforms should not build dark mode twice either. Appetite UI's [Platform Bridge](https://www.appetiteui.com/docs/platform-bridge) names the Apple and Material slots and points each one at a semantic token in Brand mode. `iOS/label` and `M3/sys/color/on-surface` both resolve through `text/strong-950`, which points at `colors/gray/950` in Light. Switch the frame to Dark and both mirrors follow the one token to `colors/gray/0`, so there is no Apple dark palette and Material dark palette to keep in step.

The platforms ask for the same thing in code. Apple's guidelines say to "embrace colors that adapt to the current appearance" and to "avoid using hard-coded color values or colors that don't adapt", and its semantic colors, such as `separator` on iOS, adapt on their own. A semantic variable with two modes is the Figma side of that rule, and it is what hands the developer one name with two values. How one file carries both platforms beyond color is in [How to design one app for iOS and Android in Figma](https://www.appetiteui.com/blog/design-one-app-for-ios-and-android-in-figma).

## Everything a mode cannot reach needs a check before handoff.

A mode switches what is bound to a variable. Everything else stays as drawn, and that is where dark mode breaks. Set the frame to Dark and look for these:

**What to check on a frame in Dark, and why it breaks**

| Check | Why it breaks | What to do |
| --- | --- | --- |
| A fill or stroke with a raw hex | only bound values switch | bind it to a semantic role |
| Shadows | black at an alpha nearly vanishes on near black | separate raised surfaces by their fill |
| Photos and illustrations | a picture has no mode | keep one if it reads in both, else draw a light and a dark version |
| Brand color on the dark ground | a blue chosen for white can drop under 4.5:1 on near black | point the Dark value at a lighter step, then check text and icons against the ground |

Apple's advice for pictures is the same as for colors: "If an asset looks good in only one mode, modify the asset or create separate light and dark assets." The shadow note comes from the [effects docs](https://www.appetiteui.com/docs/effects), which state that every shadow layer is pure black at the stated alpha.

To show both themes in a prototype, add the Set variable mode action to a tap and pick the collection and the mode. The action changes the mode of the page, and Figma's help says a mode set on an object "will still be used for itself and any nested children", so leave the screen frame on Auto for the tap to reach it. Figma allows variables in prototypes on any paid plan, so a stakeholder flips the same frame between Light and Dark.

## Code reads the same two values.

A mode that lives on the variable reaches code as a second value under the same name. In the CSS output `--ap-brand-base` is #155DFC on `:root` and #2B7FFF inside a `prefers-color-scheme: dark` block, which the [code output docs](https://www.appetiteui.com/docs/code-output) print:

**What the CSS export does with two modes** one name, two values

```css
:root {
  --ap-brand-base: #155DFC;
}

@media (prefers-color-scheme: dark) {
  :root { --ap-brand-base: #2B7FFF; }
}
```

The whole trip, SwiftUI, Jetpack Compose and Flutter included, is in [How one Figma token becomes SwiftUI, Compose, Flutter and CSS](https://www.appetiteui.com/blog/figma-token-to-swiftui-compose-flutter-css).

## Questions

### Do I need a paid Figma plan for dark mode with variables?

Yes, for the modes. Figma lists variable modes for Education, Professional, Organization and Enterprise plans. A collection without a second mode holds one value per variable, so Light and Dark on the same variable need modes.

### Should I use variants or variables for dark mode in Figma?

Variables. A Mode variant doubles the variants of every component set and leaves the switch on each instance, while a variable mode switches a whole frame from one place. Appetite UI removed its Mode variant from the iOS sets in release 2.4, and since then one switch drives dark mode across the file.

### How many modes can a Figma variable collection have?

The number of modes depends on the Figma plan. Figma's plans page lists up to 10 modes per collection on Professional, up to 20 on Organization, and unlimited modes with extended collections on Enterprise. Light and Dark need two.

### Can I switch between light and dark in a Figma prototype?

Yes. Add the Set variable mode action to an interaction, then choose the collection and the mode. The action changes the mode of the page, so a frame set to Auto follows it and a frame with its own mode keeps that mode. Figma supports variables in prototypes on any paid plan.

### What happens to images when a frame switches to dark mode?

Nothing, because a picture has no mode. Keep one picture if it reads well in both appearances; otherwise draw a light and a dark version, which is also what Apple's guidelines ask for.

### Does Appetite UI include dark mode?

Yes. Every Appetite UI component carries light and dark through the Color Tokens collection, which has a Light and a Dark mode and 161 variables, and the app screens are drawn in both. The [color docs](https://www.appetiteui.com/docs/colors) show every semantic role in both modes, and the [read-only preview](https://www.figma.com/design/iNFK9Pl2e9vEeClqhm1F4t/Appetite-UI-%E2%9C%A6-2.4?node-id=10-602) opens the whole file before you buy.

## Sources

- [Figma Learn: Modes for variables](https://help.figma.com/hc/en-us/articles/15343816063383-Modes-for-variables), read September 26, 2026
- [Figma Learn: Figma plans and features](https://help.figma.com/hc/en-us/articles/360040328273-Figma-plans-and-features), read September 26, 2026
- [Figma Learn: Variable modes in prototypes](https://help.figma.com/hc/en-us/articles/15253268379799-Variable-modes-in-prototypes), read September 26, 2026
- [Apple Human Interface Guidelines: Dark Mode](https://developer.apple.com/design/human-interface-guidelines/dark-mode), read September 24, 2026
- [Apple Developer: Supporting Dark Mode in your interface](https://developer.apple.com/documentation/uikit/supporting-dark-mode-in-your-interface), read September 24, 2026
- [W3C: Understanding WCAG 2.2, Contrast (Minimum)](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html), read September 24, 2026
