Last October, we launched our new UI redesign during Webflow Conf.
In January, we followed that up with the successful launch of new appearance settings in the Designer that allow users to choose between a default, darker, or brighter theme.
You might be asking; how did we facilitate such large-scale design updates and manage to keep the UI visually consistent? The answer: by using design tokens and CSS variables.
In this blog, we’ll explore the ins and outs of the behind-the-scenes magic that makes these large UI changes possible.
How we utilize design tokens and CSS variables
Like many companies, Webflow uses Figma to create mockups of new features. Our designers leverage design tokens – key-value pairs that use Figma’s Variables API – within their mockups.
At Webflow, we have over one hundred design tokens in Figma that cover nearly every aspect of CSS styling: colors, backgrounds, font sizes, borders, border radii, sizing, spacing, and more. We’ve carefully curated this robust collection of tokens over time through close collaboration between our Design System team and the rest of the design organization.
For example, we might have a design token for our default text color that looks like this: Text/Default: #C4C4C4
. We might also have a design token that can be used as a margin or padding value that looks like this: Space/space-1: 4px
. We even have some tokens that point to other tokens as their value.
We use these design tokens to create CSS variables in our code. For instance, the design token for our default text color that we saw earlier becomes var(--colors-text-default)
, which we can then use as the value for the CSS color property for some HTML element like this:
color: var(--colors-text-default);
That variable references an underlying color token:
--colors-text-default: var(--core-colors-gray-400);
And these color variables are then tied to our base hex values:
--core-colors-gray-400: #C4C4C4;
Usage in our code
Given that Webflow is a mature codebase that has seen several iterations of both JS-based and CSS-based styling solutions over its lifetime, implementing design tokens was not a trivial task. To further complicate matters, the codebase had numerous occurrences of hard-coded style values, which led to inconsistent styles and bespoke component designs.
We first took the time to unify our systems so that they could all use the same set of CSS variables. Then we replaced any hard-coded values to instead use those variables. The unification process took a couple months to complete. This initial work was well worth the effort though, as we now have a single source of truth for our design tokens and styles.
Our CSS-based style system using .less
files defines all of our design tokens as Less variables that can be used in other .less
files. Our JS-based style system defines all of our design tokens inside what essentially boils down to an object of key-value pairs.
This allows us to style our elements using CSS/LESS like this:
.button {
color: @d2--textDefault;
}
Or to style our elements using our internal JS-based style system like this:
const HelpText = emotionStyled('div')({
color: tokens.colors.textDefault,
});
The important thing to note here is that everything is using these tokens and variables. We don’t have any instances of hard-coded base style values anywhere anymore.
We even have tooling in place to proactively catch instances where developers have tried to add hard-coded style values in pull requests in GitHub! This tooling traverses the abstract syntax tree (AST) of the code and reports any style values it encounters that don’t use our design tokens. This way we can keep our codebase clean and prevent future regressions of non-ideal style usage.
Making style updates
Having this setup in place enables us to make large-scale UI changes fairly easily.
If we want to update our default text color to be something else, we update the design token value in one place in Figma and in one place in our code. Then, the rest just works. Because all of our frontend code is using these tokens, when a token’s color value is changed, the color gets updated everywhere all throughout our app.
That’s the dream, right? To update once and see the effects everywhere. No more hunting down hundreds or thousands of instances where a color was used and replacing them, hoping you caught them all.
This system also makes theming much easier. We mentioned earlier that the Designer allows you to choose between a default, darker, or brighter theme. To support these three themes, we have three theme config files that contain all of the design token values for each theme. The variable names are the same for each theme (and enforced by TypeScript!), but the values are different. And since our components reference the variable names in their styles, switching between each theme just works for every component. All we have to do is swap out the set of tokens and values used in our app based on the currently selected theme, and the styles get applied everywhere.
const lightThemeColors: ThemeTokens = {
textDefault: coreColorTokens.gray[800],
/* more tokens here */
};
const darkThemeColors: ThemeTokens = {
textDefault: coreColorTokens.gray[400],
/* more tokens here */
};
Moving forward
With our current system in place, we can respond to customer feedback and deliver value to the business much more quickly. We’ve enabled ourselves to wrangle a complex, sprawling codebase and make UI style updates with ease.
So, what’s next? We’re now in a place where we can continue to refine the look and feel of our app without requiring a significant time investment. Could that mean light mode in Designer? Dark mode in Dashboard? Even further theme customization? No promises, but the possibilities are endless!
We’re looking for product and engineering talent to join us on our mission to bring development superpowers to everyone.