{"id":"l-fig-3-4","courseId":"figma-ui-design","lang":"en","kind":"lesson","summary":"Add Light and Dark modes to Steady’s semantic variables, switch whole screens between them, and re-check contrast so dark mode is designed rather than inverted.","html":"<p>The quickest way to make dark mode is to duplicate every screen and recolor it by hand. It is also the quickest way to have two sets of screens that disagree within a month: someone adds a habit row to the light Today screen and forgets the dark one. In Steady, dark mode will be one switch on a frame, because the colors already point at meanings.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Modes are columns of values</h2>\n<p>Open your local variables (reached from the right sidebar with nothing selected, at the time of writing) and look at the <code dir=\"ltr\">Semantic</code> collection. It has one column of values, named <code dir=\"ltr\">Mode 1</code> by default. Rename it <code dir=\"ltr\">Light</code>, then add a mode and call it <code dir=\"ltr\">Dark</code>. Every variable now has two values: one per column. Figma copies the existing values into the new column, which is a starting point, not an answer.</p>\n<p>Fill in the Dark column by aliasing different primitives. Here is Steady’s mapping, with contrast measured against the surface each color sits on:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"Scrollable table\" tabindex=\"0\"><table><thead>\n<tr>\n<th>Semantic variable</th>\n<th>Light</th>\n<th>Dark</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code dir=\"ltr\">color/surface/default</code></td>\n<td>white</td>\n<td>gray/950 (#111827)</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">color/surface/raised</code></td>\n<td>white</td>\n<td>gray/800 (#1F2937)</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">color/text/primary</code></td>\n<td>gray/900, 17.40:1</td>\n<td>gray/50, 16.98:1</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">color/text/secondary</code></td>\n<td>gray/500, 4.83:1</td>\n<td>gray/400, 6.99:1</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">color/action/primary</code></td>\n<td>teal/700, 5.47:1</td>\n<td>teal/400, 9.53:1</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">color/on-action</code></td>\n<td>white</td>\n<td>gray/950</td>\n</tr>\n</tbody>\n</table></div><p>Keep the <code dir=\"ltr\">Primitives</code> collection at one mode. A primitive is a fact (<code dir=\"ltr\">teal/700</code> is #0F766E); a semantic variable is a decision that depends on context. Modes belong to decisions.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 230\" role=\"img\" aria-labelledby=\"l-fig-3-4-t1\"><defs><marker id=\"arrow-l-fig-3-4-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-fig-3-4-t1\">The semantic variable action primary sits in the middle. In Light mode it points to teal 700; in Dark mode it points to teal 400. The check button bound to it shows dark teal on a light screen and light teal on a dark screen.</title>\n  <rect class=\"d-box\" x=\"20\" y=\"40\" width=\"150\" height=\"44\" rx=\"8\"/>\n  <text class=\"d-code\" x=\"95\" y=\"67\" text-anchor=\"middle\">teal/700</text>\n  <rect class=\"d-box\" x=\"20\" y=\"140\" width=\"150\" height=\"44\" rx=\"8\"/>\n  <text class=\"d-code\" x=\"95\" y=\"167\" text-anchor=\"middle\">teal/400</text>\n  <rect class=\"d-box-primary\" x=\"250\" y=\"88\" width=\"180\" height=\"48\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"340\" y=\"117\" text-anchor=\"middle\">action/primary</text>\n  <path class=\"d-arrow\" d=\"M248 104 L172 66\" marker-end=\"url(#arrow-l-fig-3-4-1)\"/>\n  <path class=\"d-arrow\" d=\"M248 122 L172 160\" marker-end=\"url(#arrow-l-fig-3-4-1)\"/>\n  <text class=\"d-label-muted\" x=\"205\" y=\"72\" text-anchor=\"middle\">Light</text>\n  <text class=\"d-label-muted\" x=\"205\" y=\"164\" text-anchor=\"middle\">Dark</text>\n  <rect class=\"d-box\" x=\"500\" y=\"30\" width=\"160\" height=\"70\" rx=\"12\"/>\n  <circle class=\"d-dot\" cx=\"530\" cy=\"65\" r=\"12\"/>\n  <text class=\"d-label\" x=\"552\" y=\"70\">Light row</text>\n  <rect class=\"d-box-accent\" x=\"500\" y=\"126\" width=\"160\" height=\"70\" rx=\"12\"/>\n  <circle class=\"d-dot\" cx=\"530\" cy=\"161\" r=\"12\"/>\n  <text class=\"d-label\" x=\"552\" y=\"166\">Dark row</text>\n  <path class=\"d-arrow\" d=\"M432 104 L498 70\" marker-end=\"url(#arrow-l-fig-3-4-1)\"/>\n  <path class=\"d-arrow\" d=\"M432 120 L498 156\" marker-end=\"url(#arrow-l-fig-3-4-1)\"/>\n</svg><figcaption>The same semantic variable resolves to different primitives per mode</figcaption></figure>\n<h2 id=\"sec-2\" tabindex=\"-1\">Switching a screen</h2>\n<p>Duplicate the Today frame and select the copy. In the right sidebar, use the variable mode control (in the Appearance section at the time of writing), pick the <code dir=\"ltr\">Semantic</code> collection and choose <code dir=\"ltr\">Dark</code>. Every bound fill, stroke and text color on the screen swaps at once.</p>\n<p>That works because of inheritance. Every layer starts on <strong>Auto</strong>, which means “use my parent’s mode”. Figma walks up the layer tree until it finds a parent with a mode set, and if none has one, it uses the collection’s default mode (the first column). You can also set a mode on a whole page with nothing selected.</p>\n<p>Now the duplicate is not a copy you maintain; it is the same components viewed through a different mode. Add a habit row to the light screen and copy it into the dark one, and it is already dark.</p>\n<aside class=\"callout callout-note\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/></svg><span><span class=\"callout-kind\">Note</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Plan availability</span></p>\n<p>At the time of writing, the free Starter plan does not include variable modes; Professional allows up to 10 modes per collection and Organization up to 20. If you are on Starter, build the Dark column as a second collection to see the values side by side, and follow along with the reasoning.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Designing dark, not inverting</h2>\n<p>Dark mode is a design of its own. A few rules carry most of the weight:</p>\n<ul>\n<li><strong>Accents get lighter.</strong> Teal/700 measures 5.47:1 on white but only 3.24:1 on gray/950. The dark accent has to be a lighter shade.</li>\n<li><strong>Text on accents may flip.</strong> White on teal/400 is 1.86:1, a clear fail. That is why <code dir=\"ltr\">color/on-action</code> exists as its own token: in Dark it points at gray/950, giving 9.53:1.</li>\n<li><strong>Elevation goes lighter, not darker.</strong> Shadows barely show on dark backgrounds, so raised surfaces such as the New habit sheet use a slightly lighter gray (<code dir=\"ltr\">surface/raised</code>).</li>\n<li><strong>Avoid harsh extremes.</strong> Near-white text on near-black reads better for long sessions than pure white on pure black, which can glare.</li>\n</ul>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Trusting your light-mode contrast checks</span></p>\n<p>Every pair you measured in Lesson 1.3 was measured on white. In dark mode the backgrounds changed, so the numbers changed. Switch each screen to Dark and re-run Figma’s contrast checker on text, icons and control outlines. Pairs that pass in one mode routinely fail in the other.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">Modes beyond color</h2>\n<p>Modes are not just for themes. A <code dir=\"ltr\">Density</code> collection with <code dir=\"ltr\">Comfortable</code> and <code dir=\"ltr\">Compact</code> modes can hold number variables for row padding (12 versus 8) and row gap. A <code dir=\"ltr\">Content</code> collection with string variables can hold the same labels in English and Arabic, which is a quick way to see whether “Save habit” still fits when it becomes longer. You can even switch modes inside a prototype, for example to demo the dark theme toggle in Settings.</p>\n<p>You now have a themed, tokenised kit. The last lesson of this section organises it into something another designer, or a developer, can pick up and use without asking you.</p>\n","toc":[{"id":"sec-1","text":"Modes are columns of values","level":2},{"id":"sec-2","text":"Switching a screen","level":2},{"id":"sec-3","text":"Designing dark, not inverting","level":2},{"id":"sec-4","text":"Modes beyond color","level":2}],"takeaways":["A mode is one column of values in a variable collection; switching a frame’s mode swaps every bound value at once.","Put modes on the semantic collection and keep primitives single-mode, so dark mode only changes what each meaning points at.","Layers use the Auto mode by default and inherit from their nearest parent with a mode set, falling back to the collection’s default.","Dark mode is not inversion; accents usually get lighter, text on accents may flip to dark, and surfaces get lighter as they rise.","Every text and control color needs a fresh contrast check in each mode, because a pair that passes in light can fail in dark."],"further":[{"title":"Modes for variables","url":"https://help.figma.com/hc/en-us/articles/15343816063383-Modes-for-variables"},{"title":"Variable modes in prototypes","url":"https://help.figma.com/hc/en-us/articles/15253268379799-Variable-modes-in-prototypes"},{"title":"Dark Mode (Apple Human Interface Guidelines)","url":"https://developer.apple.com/design/human-interface-guidelines/dark-mode"}],"quiz":[{"q":"<p>You set the Today frame to Dark. One habit row inside it has its mode explicitly set to Light. What do you see?</p>","options":[{"text":"The whole screen is dark, and that one row renders with Light values.","why":"Correct. Layers inherit from the nearest parent with a mode set. An explicit mode on the row overrides the frame’s for that row and its children."},{"text":"The whole screen including the row is dark, because the frame’s mode always wins.","why":"The nearest explicit mode wins, not the outermost one."},{"text":"Figma shows an error because modes conflict.","why":"Mixing modes is allowed and useful, for example a light card inside a dark promo area."},{"text":"The row turns light and so does the rest of the screen.","why":"A child’s mode never changes its parent’s."}],"answer":0},{"q":"<p>In dark mode, <code dir=\"ltr\">color/action/primary</code> points at teal/400 (#2DD4BF) and the button label uses <code dir=\"ltr\">color/on-action</code>, still white. The label measures 1.86:1. What is the right fix?</p>","options":[{"text":"Point action/primary back at teal/700 in dark mode.","why":"Teal/700 on the dark surface is only 3.24:1, so the check buttons would lose contrast instead."},{"text":"Add a text shadow to the label in dark mode.","why":"Shadows do not count toward contrast and blur the text."},{"text":"Point on-action’s Dark value at a near-black such as gray/950, which gives 9.53:1 on teal/400.","why":"Correct. That is why on-action is its own semantic token. Its value can flip from white to dark per mode."},{"text":"Make the label bold so it counts as large text.","why":"Even large text needs 3:1, and 1.86:1 fails that too."}],"answer":2},{"q":"<p>Why do the primitives (teal/700, gray/500…) stay in a collection with a single mode?</p>","options":[{"text":"Figma does not allow modes on collections with more than 20 variables.","why":"There is no such limit. The choice is architectural."},{"text":"Primitives are raw facts about the palette. Meanings change per mode by aliasing different primitives.","why":"Correct. If teal/700 changed per mode, it would no longer be teal/700, and every alias would become unpredictable."},{"text":"Primitives cannot be aliased if they have modes.","why":"Variables with modes can still be aliased. The reason is clarity, not a technical rule."},{"text":"Dev Mode only reads single-mode collections.","why":"Dev Mode shows variables from any collection, including their modes."}],"answer":1}],"exercise":{"kind":"choice","multiple":true,"options":["lighter-accent","flip-on-action","border-dark","secondary-white","duplicate-recolor","mode-primitives"],"correct":["lighter-accent","flip-on-action","border-dark"],"title":"Review the first dark-mode pass","prompt":"<p>A teammate added a Dark mode to Steady’s Semantic collection but only changed the surface and text values. Here is what the Today screen resolves to in Dark, measured against <code dir=\"ltr\">surface/default</code> (#111827) unless noted:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"Scrollable table\" tabindex=\"0\"><table><thead>\n<tr>\n<th>Variable</th>\n<th>Dark value</th>\n<th>Result</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code dir=\"ltr\">text/primary</code></td>\n<td>gray/50</td>\n<td>16.98:1</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">text/secondary</code></td>\n<td>gray/400</td>\n<td>6.99:1</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">action/primary</code></td>\n<td>teal/700 (unchanged)</td>\n<td>3.24:1, check icons and links look dull</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">on-action</code></td>\n<td>white (unchanged)</td>\n<td>5.47:1 on teal/700</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">border/default</code></td>\n<td>gray/200 (unchanged)</td>\n<td>14.33:1, dividers glare</td>\n</tr>\n</tbody>\n</table></div><p>Select <strong>every</strong> change that improves the dark mode without breaking anything else.</p>\n","hints":["Links and small check icons are text-like. Is 3.24:1 enough for them?","If you lighten the accent, check what happens to the white label sitting on it.","Dividers are decoration; they should be visible but quiet. A near-white line on near-black is neither."],"explanation":"<p>Point <code dir=\"ltr\">action/primary</code> at a <strong>lighter teal</strong> such as teal/400 (9.53:1 on gray/950), because links and small icons need text-level contrast. That makes white labels on the accent fail (1.86:1), so <strong>flip <code dir=\"ltr\">on-action</code> to a dark gray</strong> such as gray/950 (9.53:1) in Dark. Point <code dir=\"ltr\">border/default</code> at a <strong>darker gray</strong> so dividers separate rows without glaring; keep a separate control-outline token that still meets 3:1 for checkboxes. Making secondary text white erases the text hierarchy, recoloring a duplicate frame by hand defeats the whole point of modes, and primitives should stay single-mode facts that the semantic layer chooses between.</p>\n","checkLabels":{},"optionText":{"lighter-accent":{"text":"Point action/primary’s Dark value at a lighter teal such as teal/400.","why":"Correct. Teal/700 is only 3.24:1 on the dark surface; teal/400 gives 9.53:1 and keeps the brand hue."},"flip-on-action":{"text":"Point on-action’s Dark value at gray/950 so labels on the lighter teal stay readable.","why":"Correct. White on teal/400 is 1.86:1. Dark text on it is 9.53:1, which is exactly why on-action is its own token."},"border-dark":{"text":"Point border/default’s Dark value at a darker gray so dividers are quiet.","why":"Correct. A decorative divider at 14:1 shouts. A subtle gray separates rows; control outlines that must meet 3:1 get their own token."},"secondary-white":{"text":"Set text/secondary to white in Dark so all text has maximum contrast.","why":"Secondary text already passes at 6.99:1. Making it white flattens the hierarchy between names and streaks."},"duplicate-recolor":{"text":"Duplicate the Today frame and recolor the copy by hand for dark mode.","why":"Hand-recolored copies drift from the real screen immediately. Modes exist so one screen serves both themes."},"mode-primitives":{"text":"Add a Dark mode to the Primitives collection and lighten teal/700 there.","why":"Then teal/700 would no longer mean #0F766E, and every alias using it would change unpredictably. Modes belong on the semantic layer."}}},"words":720,"readMinutes":3}