I was looking at a pull request last Monday. The marketing team had just rolled out a massive rebrand. We swapped our primary typeface from Inter to a custom geometric sans. The design team updated the core Figma variables. They ran their export script. The pull request had 142 changed files.
I opened the JSON token dictionary. Every single typography token had been updated. That sounds correct at first. But then I looked closer at the diff.
"heading-1": {
"$type": "typography",
"$value": {
"fontFamily": "CustomGeometric",
"fontSize": "48px",
"fontWeight": 700,
"lineHeight": "56px"
}
}
This pattern was repeated 45 times for every heading, body, and caption style. The export script had completely flattened the references. The semantic link to our base {font.family.primary} token was gone.
We were back to hardcoded values. They were just hidden inside a JSON file this time. If we ever wanted to swap the font again, we would have to rely on a script to blindly find and replace strings. That is incredibly fragile.
The anatomy of a broken token pipeline
A design system is basically a giant dependency graph. Your base tokens define the raw values. Your semantic tokens give those values meaning. Your component tokens apply that meaning to specific UI elements.
When you flatten your output, you destroy the graph.
A proper W3C typography token should preserve the aliases. It should look like this instead.
"heading-1": {
"$type": "typography",
"$value": {
"fontFamily": "{font.family.primary}",
"fontSize": "{font.size.4xl}",
"fontWeight": "{font.weight.bold}",
"lineHeight": "{line.height.tight}"
}
}
This tells the style dictionary exactly how things relate. If the primary font family changes, you only update one single node in the entire JSON file. The W3C specification specifically designed the $type: typography composite token to handle this exact scenario.
Turns out extracting this from Figma is surprisingly tricky.
Reading bindings from the Figma API
Figma stores text styles and variables differently. A text style is a bundle of properties. Recently Figma allowed designers to bind variables directly to the properties inside those text styles.
To preserve the aliases, you cannot just read the raw fontFamily or fontSize from the text node. You have to check if a variable is bound to that specific property first.
Here is how you actually extract the bindings using the Figma Plugin API.
function extractTypographyValue(style: TextStyle): TypographyTokenValue {
const boundVariables = style.boundVariables;
const result: TypographyTokenValue = {
fontFamily: style.fontName.family,
fontSize: `${style.fontSize}px`,
fontWeight: style.fontName.style,
letterSpacing: formatLetterSpacing(style.letterSpacing),
lineHeight: formatLineHeight(style.lineHeight)
};
if (!boundVariables) {
return result;
}
if (boundVariables.fontFamily) {
result.fontFamily = resolveVariableAlias(boundVariables.fontFamily.id);
}
if (boundVariables.fontSize) {
result.fontSize = resolveVariableAlias(boundVariables.fontSize.id);
}
return result;
}
The resolveVariableAlias function is the crucial part here. You need to look up the variable by its ID and format its name into a W3C alias string. You wrap it in curly braces and replace any slashes with dots.
function resolveVariableAlias(variableId: string): string {
const variable = figma.variables.getVariableById(variableId);
if (!variable) {
throw new Error(`Variable not found for ID: ${variableId}`);
}
const cleanName = variable.name.replace(/\//g, ".");
return `{${cleanName}}`;
}
This keeps the reference intact. The token file becomes a true reflection of the Figma dependency graph.
Normalising weird Figma units
You probably noticed the formatting functions in the first code block. This is the second major trap when exporting typography. Figma uses some very specific internal units that do not translate directly to CSS.
Line height is the biggest offender. Figma can define line height in pixels or as a percentage. CSS prefers unitless numbers or rem units for line height to maintain accessibility and scaling.
If you just export the raw Figma object, you get bizarre output that breaks your stylesheets. You have to normalise it.
function formatLineHeight(lineHeight: LineHeight): string | number {
if (lineHeight.unit === "AUTO") {
return "normal";
}
if (lineHeight.unit === "PERCENT") {
return Math.round((lineHeight.value / 100) * 100) / 100;
}
if (lineHeight.unit === "PIXELS") {
return `${lineHeight.value}px`;
}
return "normal";
}
Letter spacing works similarly. Figma allows percentage based letter spacing. CSS does not understand percentage letter spacing out of the box. You usually want to convert it to em units so it scales relative to the font size.
function formatLetterSpacing(letterSpacing: LetterSpacing): string {
if (letterSpacing.value === 0) {
return "0";
}
if (letterSpacing.unit === "PERCENT") {
const emValue = letterSpacing.value / 100;
return `${emValue}em`;
}
return `${letterSpacing.value}px`;
}
This normalisation step is absolutely vital. If you skip it, your developers will end up writing massive transformation scripts on their end before they can even feed the tokens into Style Dictionary or Tailwind.
Stripping internal collection paths
There is one more detail that always causes friction. Figma organises variables into collections. Designers might name a collection "Brand Core" or "Semantic Tokens".
When you read the variable names from the API, those collection names often get bundled into the path. You end up with an alias like {brand.core.font.family.primary}.
The developers do not care about Figma collections. They just want the clean token name. You need to strip those internal grouping labels out of the final output paths. Your parsing logic should detect the root collection and slice it off before generating the W3C alias string.
It keeps the JSON clean. It stops internal design team organisation choices from polluting the production codebase.
Fixing the pipeline for good
I spent way too much time writing custom scripts to handle these edge cases. Every time Figma updated their API, the script would break. That is exactly why I built these normalisation rules directly into Design System Sync.
We just shipped Text Styles Export v2. The plugin now exports your Figma text styles as first-class W3C tokens with $type: typography. It preserves every single bound alias. It normalises the line heights and letter spacing perfectly for CSS. It strips the internal collection names automatically.
We also added a new Studio tier for engineers who work across multiple machines. It gives you multi-device access and priority support. Plus we shipped a Connect Unwired feature in the Pro tier that detects hardcoded hex codes and wires them to your existing tokens in a single click.
If you want to stop fighting with flattened JSON files, you can grab the plugin here: https://ds-sync.netlify.app?utm_source=devto&utm_medium=post&utm_campaign=bot (or find it on Figma Community: https://www.figma.com/community/plugin/1561389071519901700?utm_source=devto&utm_medium=post&utm_campaign=bot).
Honestly, handling the technical side of tokens is only half the battle. Getting the team aligned on naming conventions and governance is usually much harder. I just published a 200 page companion handbook called "Ship Your Design System" to cover all of that. It has 23 chapters across foundations, components, and governance workflows. It is basically the manual I wish I had when I started building these pipelines. You can find the book at https://alexanderburgos.netlify.app/books/ship-your-design-system.
Check your current token output. If you see hardcoded strings instead of curly braces in your typography tokens, it is time to upgrade your export logic. Your future self will thank you.
Top comments (0)