~/wiki / dizayn-sistema-i-komponenty / tyomnaya-tema-arkhitektura-cherez-peremennye

Dark theme in one day – if you set the variables correctly in advance

Main chat

A chat for vibe coders: news, guides, live cases, marketplace, and finding executors.

$ cd section/ $ join vibe dev
Dark theme in one day – if you set the variables correctly in advance - обложка

There are two ways to add a dark topic. One: you open each component, you change colors manually, you watch half crumble, you redo three days, you still miss something. Second, you create Dark Mode in Figma Variables, you set the dark values, you switch -- everything gets updated in a second. One day instead of three.

The difference is in one decision made at the beginning: use semantic tokens or not.


A dark topic is not an inversion

The most common mistake is to make a dark theme as mechanically inverted colors light. The result looks strange - and it's not an accident.

** Optical vibration.** Saturated colors vibrate against a dark background - this is a physiological effect. #E94560 looks energetic on a white background. The same color on #1A1A2E is aggressive and uncomfortable. Accents in a dark topic should be made less saturated or slightly lighter.

*box-shadow: 0 4px 12px rgba(0,0,0,0.1) - Dark shadow on a light background creates depth. It's almost invisible in the dark. In a dark theme, elevation is shown through the lightness of the surface: what is “above” is lighter.

**Contrast ratio changes. ** Color that passes WCAG AA 4.5:1 in a light theme may not pass in dark. Especially gray shades. After adding a dark theme, a separate check of each text element.


Variables: Light and Dark Mode Architecture

Figma Variables, Collections, two Mode: Light and Dark. Each component uses a semantic token—not a specific color. When you switch Mode, the values change under the same names.

plaintext
Collection: Color System
Modes: Light | Dark

color/background/page:
Light: #FFFFFFFF
#0F0F1A is darker than the surface

color/background/surface:
Light #F5F5F0
Dark #1A1A2E

color/background/elevated
Light → #FFFFFF (+ shadow)
Dark → #252540 ← Lighter surface, elevation through brightness

color/text/primary:
Light #1A1A2E
Dark: #F5F5F0

color/text/secondary:
Light #4A4A6A
Dark #9090B0

color/text/disabled:
Light: #AAAACC
Dark #5070

color/brand/accent:
Light #E94560
Dark → #D4304A ← A little darker, removes the vibration

color/border/default:
Light #E0E0F0
Dark → rgba(255,255,255.0.1)

color/status/error:
Light #E94560
#FF6080 is slightly lighter for viewing in the dark

Elevation through lordship, not shadows

In a dark topic, the card should “rise” above the background of the page – but the shadows don’t work. Solution: Each level of elevation is slightly lighter.

css
:root[data-theme="dark"] {
  --color-background-page:     #0F0F1A;   /* самый тёмный */
  --color-background-surface:  #1A1A2E;
  --color-background-elevated: #252540;   /* карточки, модали */
  --color-background-overlay:  #303060;   /* тултипы, dropdown */
}

.card {
  background: var(--color-background-elevated);
  border: 1px solid rgba(255, 255, 255, 0.08);
  /* box-shadow убирается в тёмной теме */
}

Components that always break down

Inputs. Placeholder loses contrast against a dark background - often does not pass 3:1. Border in the idle state is almost invisible. Check both.

PNG with transparency. Logos with white details on a dark background look strange. Options: SVG which is stylized through CSS (fill: currentColor), or <picture> with a separate version for a dark theme.

Graphics. Chart.js/Recharts colors are usually hard-to-config. Need a hook:

javascript
function useChartColors() {
  const style = getComputedStyle(document.documentElement)
  return {
    primary:   style.getPropertyValue('--color-chart-primary').trim(),
    secondary: style.getPropertyValue('--color-chart-secondary').trim(),
    grid:      style.getPropertyValue('--color-chart-grid').trim(),
  }
}

Icons with hard-cut fill. fill: #1A1A2E is invisible against a dark background. fill: currentColor works automatically. Do a separate audit of the icons.

Modal overlay. rgba(0,0,0,0.5) on a dark background gives almost no separation from the content. You need rgba(0,0,0,0.7) or backdrop-filter: blur(4px).


CSS: Switching without flash

css
:root {
  --color-background-page:    #FFFFFF;
  --color-background-surface: #F5F5F0;
  --color-text-primary:       #1A1A2E;
  --color-brand-accent:       #E94560;
  --color-border-default:     #E0E0F0;
}

/* Системный режим (без ручного переключения) */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --color-background-page:    #0F0F1A;
    --color-background-surface: #1A1A2E;
    --color-text-primary:       #F5F5F0;
    --color-brand-accent:       #D4304A;
    --color-border-default:     rgba(255,255,255,0.1);
  }
}

/* Ручное переключение */
[data-theme="dark"] {
  --color-background-page:    #0F0F1A;
  --color-background-surface: #1A1A2E;
  --color-text-primary:       #F5F5F0;
  --color-brand-accent:       #D4304A;
  --color-border-default:     rgba(255,255,255,0.1);
}

/* Плавный переход — ТОЛЬКО цвета, не всё */
*, *::before, *::after {
  transition: background-color 0.2s ease, color 0.15s ease,
              border-color 0.15s ease;
}

transition: all will break existing animations. Only color properties.


When to start: before or after launch

If the product is not yet launched, build with a dark theme right away. With the right token architecture, it’s almost free: add Dark Mode to Variables and set values.

If the product is already running without tokens – first transfer to Variables, then add a dark theme. Trying to add a dark theme without first translating into variables is reworking each component twice. It's twice as expensive.

A dark topic is a good reason to clean up the system: for it to work, everything must be on semantic tokens. Hardcut values will come out immediately. It's not a problem - it's a chance to fix debt and deal with it.


Prompt for Codex/Claude Code

markdown
Add support for a dark topic to the project.

Dark theme tokens:
- background/page: #0F0F1A
- background/surface: #1A1A2E
- background/elevated: #252540
- text/primary: #F5F5F0
- text/secondary: #9090B0
- brand/accent: #D4304A (NOT #E94560 - vibrating in the dark)
- border/default: rgba(255,255,0.1)
- status/error: #FF6080

Requirements:
1. All values through CSS Custom Properties (no hard-cut hex)
2. Support for prefers-color-scheme and handheld [data-theme="dark"]
3. Switch button in the header, save the choice in localStorage
4. In a dark topic: elevation through the lightness of surfaces, not box-shadow
5. transition only for color properties (not transition: all)

After addition:
Check contrast ratio text/primary on each background (goal ≥ 4.5:1)
- Replace box-shadow cards with border: 1px solid rgba(255,255,0.08)
Find icons with hard-cut fill and replace them with currentColor
Check the placeholder in inputs (contrast ≥ 3:1)

How a dark topic breaks things that seemed reliable

Experience teams who have added a dark theme without preparation: at first it seems like it works. Then the bugs start to appear.

Most unexpected: custom icons painted in Figma and exported as SVG with hard-cut fill="#1A1A2E". In the bright theme – dark icons on a light background. In the dark theme there are dark icons on a dark background. Invisible. And no one thought about it when they created it.

The second surprise is emails and PDFs that are generated from the product. They use light colors because they type or look in email clients that don't support dark mode. Tokens don’t help here – you need separate rules for those contexts.

Third: screenshots and previews that the product generates (preview links in chats, thumbnails). They are usually rendered in a headless browser that ignores user preferences. You need to clearly set a bright topic for these scenarios.

None of this makes a dark topic impossible. Just check all the contexts in which the product is being used – not just the main application.


Dark theme as a moment to test accessibility

According to WCAG checks, the majority of accessibility problems with contrast are detected by adding a dark theme. Because in a light theme, the team is used to specific colors and doesn't check them - they "work that way.".

When adding a dark theme, check each background/text combination. A good tool for this: the Stark plugin in Figma – it checks the contrast directly in the Figma file when switching to Dark Mode. Chrome DevTools also shows contrast ratio when inspecting text elements.

The minimum set of what to check: the main text on each background (page, surface, elevated), secondary text, placeholder in inputs, text in the buttons of all options, text of status messages (error, success, warning).


Frequent questions about a dark topic

"Should we support a dark theme if we don't have enough users on mobile?" Dark theme isn't just for mobile. Desktop users on macOS and Windows also use system dark mode. According to data from 2025-2026, about 55% of desktop users have included a dark theme in their system settings.

“Should I test every screen in a dark theme?” But not every component is tested in Storybook. Each screen should be viewed visually when switching to Figma Dark Mode, and then verified in the browser with [data-theme="dark"].

"What to do with users who have an old browser without support for prefers-color-scheme?" They have a light default theme. Dark theme through prefers-color-scheme is a progressive improvement. Manual switching over [data-theme] works everywhere JavaScript works – that is, in 99.5% of browsers.

"How does dark theme affect performance?" CSS Custom Properties switch instantly - one attribute on :root. No extra loading, no flickering (if the transition is set up correctly). Performance is identical to the light theme.

plaintext
● All components on Variables (no hard-cut hex)
Dark Mode created in Figma Variables
Accents in a dark topic: less saturated
Elevation: cards lighter than background, without box-shadow
Contrast text ≥ 4.5:1 in a dark topic
Placeholder in inputs ≥ 3:1
PNG with transparency checked
Icons: fill: currentColor instead of hard-cut color
● Switching without flash (transition only color properties)
$ cd ../ ← back to Design system and components