Skip to main content

Mapping Figma Variables to Email Design Tokens

Sync a design tool's variables into an email token set: name mapping, the properties email cannot express, light/dark modes, and a review step that catches drift.

Design tools export variables that assume a browser: relative units, opacity layers, shadows, gradients and per-component overrides. Email can express roughly half of them. This guide covers the mapping layer that translates what survives, drops what does not, and fails loudly rather than silently emitting something a client will ignore.

Root Cause: Two Systems With Different Vocabularies

A design tool's variable set describes intent in a medium with a full CSS implementation behind it. An email token set describes what can be written into a style attribute and reliably rendered by a word-processing engine. The overlap is large but not complete, and the gaps are not obvious from the variable names.

Colour and spacing translate cleanly. Type is close, provided sizes are absolute. Everything expressing depth or state — shadows, blur, hover colours, focus rings — has no representation in email at all. Opacity is the trap: it exists in CSS and is honoured by some clients, so a naive export produces a value that renders in Apple Mail and disappears in Outlook, which is worse than not exporting it.

The correct posture is that the design tool is authoritative for values and the email token set is authoritative for what exists. A variable with no email equivalent should fail the sync rather than be exported and quietly ignored.

Variable categories and their email equivalents Colour and spacing translate directly, type translates with constraints, and depth or state variables have no email representation. Half the Vocabulary Crosses Over translates directly colour — hex only spacing — px only border radius export as-is translates with rules font size — absolute, ≥14px line height — px, paired font family — plus fallback convert, then validate no equivalent shadows, blur hover and focus states opacity — partial support fail the sync, do not emit
Opacity is the dangerous column entry: it half-works, so exporting it produces a design that is correct in some clients and broken in others.

The Exact Mapping Layer

The sync reads the design tool's variables, applies a mapping table, validates each result against the email constraints, and refuses to write output if anything fails.

// sync/figma-to-tokens.js — pull variables, map, validate, then write.
// Refusing to emit is the point: a silently dropped variable is worse than a
// failed sync, because the design and the template then disagree invisibly.

const MAP = {
  // designToolName: emailTokenName
  "color/text/primary":    "colorTextBody",
  "color/text/secondary":  "colorTextMuted",
  "color/surface/default": "colorSurface",
  "color/action/default":  "colorActionBg",
  "color/action/on":       "colorActionText",
  "space/2":               "spaceStack",
  "space/4":               "spaceSection",
  "radius/default":        "radius",
  "type/body/size":        "fontBody",
  "type/body/line":        "lineBody",
};

// Categories email cannot express. Present in the design set is fine; mapped
// into the email token set is not.
const UNSUPPORTED = [/^effect\//, /^shadow\//, /state\/(hover|focus|active)/, /opacity\//];

function validate(name, value) {
  const errors = [];
  if (/^color/.test(name)) {
    // Only 6-digit hex: rgba() with alpha is unreliable and named colours
    // are rewritten by several clients.
    if (!/^#[0-9a-f]{6}$/i.test(value)) errors.push(`${name}: not a 6-digit hex (${value})`);
  }
  if (/^(font|line|space|radius)/.test(name)) {
    if (!/^\d+px$/.test(value)) errors.push(`${name}: not an absolute px value (${value})`);
  }
  if (name === "fontBody" || name === "fontSmall") {
    // The mobile inflation floor — below this, clients enlarge the block.
    if (parseInt(value, 10) < 14) errors.push(`${name}: below the 14px floor (${value})`);
  }
  return errors;
}

export function mapVariables(variables) {
  const out = {};
  const errors = [];

  for (const [designName, value] of Object.entries(variables)) {
    if (UNSUPPORTED.some((re) => re.test(designName))) continue;   // ignored by design

    const emailName = MAP[designName];
    if (!emailName) {
      // An unmapped variable is a decision nobody has made yet, not a value
      // to guess at. Surface it rather than dropping it.
      errors.push(`unmapped design variable: ${designName}`);
      continue;
    }
    errors.push(...validate(emailName, value));
    out[emailName] = value;
  }

  if (errors.length) {
    throw new Error(`token sync failed:\n  ${errors.join("\n  ")}`);
  }
  return out;
}

The explicit MAP is deliberately not a naming convention. Automatic derivation — replacing slashes with camel case — looks tidy and breaks the moment a designer renames a variable, silently producing a new token rather than an error. An explicit table turns a rename into a failed sync, which is the correct outcome because a rename is a decision someone should confirm.

Handling Light and Dark Modes

Design tools express modes as separate collections of the same variables. The email token set needs both values in one place, because both literals must reach the message — the light value inline and the dark value in a media block.

The sync therefore reads each mode and merges them into paired tokens, rather than producing two independent token sets. Where a variable exists in one mode and not the other, the sync should fail: a semantic colour with no dark value will render as its light value against a dark surface, which is the exact defect the pairing exists to prevent.

Merging modes into paired tokens A light collection and a dark collection of the same variable are merged into one email token holding both values. Two Modes In, One Paired Token Out light collection color/text/primary dark collection color/text/primary merge by name both modes required, or the sync fails colorTextBody { light: ..., dark: ... } one token, two literals
A missing dark value is a sync failure rather than a default, because the fallback — the light value on a dark surface — is the defect itself.

Variant: One-Way Sync Only

It is tempting to make the sync bidirectional so a developer's correction flows back into the design tool. Resist it. The two systems have different constraints, and a value that is correct in email — a darkened muted grey that clears 4.5:1 against white — may be deliberately lighter in the design system for use on a tinted surface that email never renders.

Keep the flow one-way and handle disagreements as conversations rather than as merges. Where email's constraints force a different value, the correct resolution is usually a new design variable for the email context, not an edit to the shared one.

Reviewing the Diff

The sync's output is a pull request containing changed values, and reviewing it well takes a specific kind of attention. A diff of hex codes tells you nothing about consequences, so the review needs two supporting artefacts.

The first is a rendered comparison. Generating the library's example templates at both the old and new token values, and attaching them to the pull request, turns an abstract change into something a reviewer can look at. A colour shifting by two hex digits may be imperceptible or may take a button below the contrast threshold, and only the rendered version distinguishes them.

The second is an automated contrast assertion. Every semantic pairing that will appear as text on a surface — body on surface, muted on surface, action text on action background, and the same set again in the dark mode — should be checked against the WCAG ratios as part of the sync. A designer adjusting a grey for a product screen has no reason to know it is also the footer colour in every transactional email, and the assertion is what surfaces that before the value ships rather than after.

Both artefacts also change who can review the pull request. With a rendered comparison and a contrast report attached, a designer can approve it directly; without them, only someone who can mentally map hex values to rendered output can, which in practice means the change waits for one specific engineer.

What a reviewable token pull request contains The raw value diff is accompanied by a rendered before-and-after comparison and an automated contrast report. A Hex Diff Is Not a Reviewable Change the raw diff alone colorTextMuted changed Nobody can tell whether this is safe. + rendered comparison before and after, side by side A designer can approve it without reading code. + contrast report every pairing, both modes Catches the grey that also happens to be the footer.
The two attachments also widen who can approve the change, which is what keeps the sync from queueing behind one person.

Pipeline Integration

Run the sync as a scheduled job that opens a pull request rather than as a step that commits directly. The diff is the review artefact — a changed hex value in a pull request is visible and discussable, whereas an automatic commit reaches the published token package with nobody having looked at it.

Fail the job loudly on any validation error and include the full error list in the notification. A sync that half-succeeds is the worst outcome: some tokens updated, some not, and no single place recording which.

Validation and Deployment Checklist

Frequently Asked Questions

Should designers work in a separate email-specific file?

A separate collection within the same file usually works better than a separate file. It keeps the brand primitives shared while letting the email context define its own semantic layer with values that respect the constraints — a darker muted grey, a larger minimum size — without those choices leaking into the product design system.

How do we handle a colour that fails contrast only in email?

Add an email-specific semantic variable rather than changing the shared one. The web version may be used on a tinted background where it passes; email renders it on white or on a dark surface where it does not. Two variables with a documented relationship is clearer than one variable that is subtly wrong in one context.

Can the sync generate the components too?

Not usefully. Components encode client workarounds — border-as-padding, conditional wrappers, attribute duplication — that no design tool models, so generated components would need hand-editing immediately and would then be overwritten on the next sync. Sync the values; author the components.

What happens when a designer adds a variable we have not mapped?

The sync fails with the variable named, which is the intended behaviour. Adding it to the mapping table is a small, deliberate act that puts a person in the loop for the decision of what the new value means in email — including whether it can be expressed there at all.

How often should the sync run?

Weekly is usually enough, plus on demand before a design change ships. Running on every design-file save produces a stream of pull requests for work-in-progress values, which trains reviewers to close them without reading — the opposite of what the review step is for.


← Back to Email Design Tokens and Component Libraries