skip to content

In a component library, how should a component map its visual variants to theme tokens, and why keep that mapping in one declared table?

level: middleimportance: should knowfreq 40%

answer

  1. variant by part by state
  2. the table names roles, not values
  3. gaps become visible
  4. no computed colors
  5. one row per new variant

basics

~20 s

Each variant should resolve, per anatomy part and state, to a semantic token name, never to a value. One declared table makes every combination visible and checkable, keeps values in the theme, and turns a new variant into one reviewed row.

solid answer

~50 s

A **variant** is a named, designed alternative look of one component — a goal meter's standard, final-push and goal-met looks, for example. The mapping should say, for each variant, which **semantic token** each anatomy part (fill, track, label, icon) reads in each state. It names roles only; the active theme supplies the values, so the table does not change when a theme is added. Keeping it in one declared table, rather than conditionals spread through the component, makes every variant-part-state combination visible, lets a check fail when a combination is missing, gives designers and native teams one artefact to compare against, and turns a new variant into a single reviewed row. The common failures it prevents are forgotten states (a hover that falls back to the wrong variant) and colors computed inside the component, which bypass the theme.

code

pseudocode · 15 lines
pseudocode
GOAL_METER_STYLES = {
  standard:  { track: "surface.sunken", fill: "progress.fill",    label: "text.default"  },
  finalPush: { track: "surface.sunken", fill: "emphasis.strong",  label: "text.emphasis" },
  goalMet:   { track: "surface.sunken", fill: "feedback.success", label: "text.default"  }
}

function checkComplete(table, parts):
  for each variant in table:
    for each part in parts:
      if table[variant][part] is missing:
        fail("goal meter: no role for " + variant + "." + part)

function styleFor(variant, part, theme):
  role = GOAL_METER_STYLES[variant][part]
  return theme.valueOf(role)

go deeper

for a junior

Recall that a variant resolves each part to a semantic role, never to a raw value, and that the theme supplies the values.

for a middle

Explain the variant-part-state grid, why a declared table exposes gaps that conditionals hide, and why computed colors inside a component bypass the theme.

for a senior

Show how you would migrate a component with scattered variant logic to a checked table and share it with native and design without breaking consumers' looks.

for a principal

Treat the mapping as the contract between design, the web library and native teams, and decide how new variants are proposed, reviewed and retired across them.

## What the mapping is A **variant** is a named, designed alternative look of one component. How consumers choose a variant — the shape of the component's API — is a separate question; this one is about what happens *after* a variant is chosen: which theme values each part of the component reads. Three dimensions meet here: - **Variant** — the designed alternative (standard, final-push, goal-met). - **Anatomy part** — each styled piece of the component (track, fill, label, milestone icon). - **State** — interaction or status (rest, hover, focus, disabled). The **variant-to-style mapping** fills that grid with **semantic token names**. It never holds a value. The theme supplies values; the mapping only decides which role each part plays in each variant. ## A worked example: a fundraising goal meter A charity donation site shows a goal meter on every campaign page. Design specifies three variants: *standard* for most of a campaign, *final-push* for its last days, *goal-met* once the target is reached. | Part | standard | final-push | goal-met | |---|---|---|---| | Track | `surface.sunken` | `surface.sunken` | `surface.sunken` | | Fill | `progress.fill` | `emphasis.strong` | `feedback.success` | | Label text | `text.default` | `text.emphasis` | `text.default` | | Milestone icon | `icon.subtle` | `emphasis.strong` | `feedback.success` | The same table, extended with a state column where states differ, is the whole styling story of the component. A new theme changes what `emphasis.strong` looks like; the table does not move. ## Why one declared table beats scattered conditionals 1. **Every combination is visible.** Reviewers read the look of the whole component in one place instead of reconstructing it from branches. 2. **Gaps can be checked.** Because the grid is data, a check can fail when a variant lacks a part or a state. With conditionals, a missing branch silently falls through to another variant's style. 3. **Values stay in the theme.** A table of role names cannot hold a literal or a computed color by accident. 4. **It is shareable.** Designers compare it with the design library's variants; a native mobile team implements the same table against its own theme object. Web and native then differ in rendering, not in decisions. 5. **Change is local.** A new variant is one row, reviewed as a design decision. Retiring one is deleting a row and seeing what referenced it. ## Failures the table prevents - **Forgotten states.** Conditionals often handle the rest state of each variant but let hover or focus fall through to the standard look, so the final-push meter flickers back to standard on hover. - **Computed colors.** Deriving the final-push fill by darkening the standard fill inside the component bypasses the theme: in another theme or mode the result can be off-palette or illegible, and no one reviewed it. - **Appearance-named variants.** A variant called *red* is a promise about a value that the next theme may break; a variant named by intent (*final-push*) survives any palette. - **Cross-variant leaks.** A branch that copies one variant's value into another ties the two together invisibly. ## Adding the state dimension States rarely differ for every part, so the table records the cells that change. For the goal meter, focus uses `border.focus` on the track in every variant, and a disabled meter — shown on an archived campaign — maps fill and label to `surface.disabled` and `text.disabled` whatever the variant. Writing these as explicit rows (*any variant, disabled*) rather than leaving them implicit is what lets the completeness check tell a deliberately shared cell from a forgotten one. A cell that intentionally repeats the rest-state role should name that role anyway, so a reviewer never has to guess. ## Variants, overrides and states compared | Mechanism | Who decides | Holds | Changes when | |---|---|---|---| | Variant mapping | library and design | role names per part and state | a designed look is added or retired | | Component override token | consumer, locally | one property, defaulting to a role | a consumer has a local need | | Theme | system owners | values for every role | brand, mode or season changes | The mapping, the overrides and the theme each own one decision, and none of them duplicates another. That is what makes the component predictable: to know how a final-push meter looks under a winter appeal theme, read the row, then look up the roles.

  • Design wants the final-push fill to be 'a bit darker than standard'. How do you handle it?
    Ask which role expresses that intent in every theme, not in one. If an existing role such as strong emphasis fits, map to it. If not, request a new semantic role so each theme defines its own darker value and it can be checked for contrast. Computing it inside the component bypasses the theme and was never reviewed in other modes.
  • How does the table help a native mobile team building the same goal meter?
    They implement the same variant-part-state grid against their own theme object, so both platforms make identical decisions and differ only in rendering. When design adds a variant, both teams add the same row, and a mismatch shows up as a diff between two tables rather than a visual surprise.

saying these in an interview costs you the question

  • Variant styles are clearest as inline conditionals next to each part.
  • The mapping table should store final colors so it documents the real look.
  • Deriving a variant by darkening another variant's color keeps the palette consistent.
  • Only the rest state needs a mapping; other states inherit sensibly.
  • Naming a variant by its color is fine because the look is what it represents.