Token fallbacks

4 min read

How do you fix a plane while it is still in the air?

This is the exact challenge of managing a design system in a living, breathing product. You cannot simply stop every product team, pause every feature, and freeze all code just to implement a new token system. The business keeps moving, and the design system has to find a way to move with it.

Context

Inside a large product team, things are often moving at different speeds. While I was recently working on revamping our design tokens in a separate Figma branch, the feature teams were already busy designing new features using the old system.

Part of this revamp was moving away from primitive tokens. The previous system used raw colour names like gray-800 for everything. I was introducing semantic tokens (tokens named for their purpose, like button-bg or tooltip-bg) even if they currently pointed to the same gray-800. This shift allows us to theoretically change the tooltip’s look in the future without accidentally changing the button as well.

The handover to developers was coming up fast, but these new semantic tokens were not quite ready to be released into the codebase yet.

The problem

Feature developers are almost always busy. If they build a feature today using current tokens that the product is already using, the chances of them going back in three months just to swap for new tokens are very low. It is a low-priority task that usually gets buried in the backlog.

Of course, this is fine for the standardised components they consume. These will be updated automatically by the design system team later. But not everything is a component. Elements like spacing between items, specific card paddings, or a unique corner radius are often defined directly in the feature’s style files.

If we launched the new features with these “danger zone” hardcoded values or old primitive tokens, we would lose the ability to manage the product globally. If we decided to change that corner radius to 12px later, we would have to hunt down every single manual instance.

The solution

The answer lay in a simple but powerful trick. Fallback values.

I suggested that the developers should start using the new token names immediately, even if those tokens did not technically exist in the code yet. They would write their code with a “safe fallback” that points to the current value.

On the web, this is as simple as adding a second value inside the variable function:

border-radius: var(--card-radius, 8px);

The browser looks for the token first. If it cannot find it, it gracefully defaults to the 8px value.

On iOS, developers can use a similar approach by providing a default value if a specific colour or token is missing from the asset catalogue:

.cornerRadius(Token.radius("card", fallback: 8))

In Android, the team can use a lookup function that checks for the new token and falls back to the legacy theme value if it is not yet defined:

Modifier.clip(RoundedCornerShape(Token.radius("card", fallback = 8.dp)))

Why this works

This approach solves two problems at once:

  1. It works today: Because the tokens are not defined yet, the apps simply ignore the empty slots and use the fallback values. The feature looks perfect at launch using the current styles.
  2. It is future-proof: When we eventually release the new design system tokens into the main codebase, the apps will see that the tokens are now defined. They will automatically respect the new values and ignore the fallbacks.

The takeaway

Building a design system is not about reaching a perfect “end state” where everything is suddenly tidy. It is about creating bridges between where the product is now and where you want it to be.

By using tokens with fallbacks, you allow the product to keep breathing while ensuring that, when the design system is ready, the product is already prepared to listen.