Scaling a design system with Tailwind CSS
Leverage Tailwind CSS and Figma’s built-in design tools to create a scalable and future-proof design system
“If you can suppress the urge to retch long enough to give it a chance, I really think you’ll wonder how you ever worked with CSS any other way.” Adam Walthan, Creator of Tailwind CSS
I’m not going to lie, I really didn’t want to like Tailwind when I first worked with it. The naming schemes are inconsistent. Its options are restrictive. It’s bloated. It doesn’t play well with custom animations. All of these thoughts went through my head when I first worked with Tailwind. But, as its creator said, after I gave it a long enough chance, it’s hard to see myself styling components in any other way. I’ve come to really appreciate what it has to offer and how well it fits together with what we as developers come to expect from a design system.
The basics of a design system
First off, what is a design system? A design system is a collection of reusable components that are often built from the ground up. In 'Atomic Design' by Brad Frost, the author takes inspiration from chemistry to create a cohesive system and experience. Smaller components like buttons and text combinations come together to build molecules, which then build even more complex organisms, then into full templates or pages. This system allows designers to take advantage of building quickly with modularity and developers won’t need to write custom code for every page.
At least, that’s what a design system should be in theory. In every design system I’ve worked on, there are decisions made by either the designer or the developers to deviate from the system, and there are sometimes good reasons for it! Sometimes it’s more accessible to opt for native elements like dropdowns, or sometimes we just want to use a different shade of a brand colour. Sometimes, the system just feels restrictive and that we’re designing around it rather than letting it help us.
When these issues inevitably arise, a system like Tailwind can scale to gracefully catch and handle them.
Colours in Tailwind CSS
One of the most basic things in a design system, arguably even more atomic than a basic component, is colours. Colours dictate a brand’s image and often come from a more abstract source than a web design team.
In the traditional way of using CSS (or a CSS pre/post-processor like Sass or Less), we can take advantage of CSS variables and reuse colours that way. For example, if we wanted to define a specific blue-green as a primary colour, we might define this CSS variable as so:
--primary: #4381DD;
--primary-light: #96BFFF;
![](https://www.nearform.com/media/pages/digital-community/scaling-a-design-system-with-tailwind-css/7b5854cc5a-1738756096/scaling-a-design-system-with-tailwind-css-primary-primary-light-1600x.png)
And use it as so:
color: var(--primary);
background-color: var(--primary-light);
And there isn’t necessarily anything wrong with doing things that way. In fact, under the hood, Tailwind creates variables like this for a variety of their properties. It helps keep things readable when developers go in to inspect and debug elements. It’s also easy enough to list a few colours, a dark grey/black, and maybe a white to throw into a colour system, but in my experience, there’s always an asterisk involved.
Sometimes, designers want to have different opacities. Sometimes, if a company is large enough, sub-brands of the company can start to have their own colour palettes that feel related to the main brand but are a slightly different shade.
--primary: #4381DD;
--primary-light: #96BFFF;
--primary-lighter: #DEEBFF;
--primary-lightest: #EBF3FF;
--primary-even-lighter-lightest-last-one-i-promise: #F0F6FF;
![Example of CSS primary colour shades](https://www.nearform.com/media/pages/digital-community/scaling-a-design-system-with-tailwind-css/73172175e5-1738756256/blog-scaling-a-design-system-with-tailwind-css-primary-examples-1600x.png)
Ok, I’m being a little facetious in the last one, but this list of variables can start to get really long and even live in different files, leading to CSS scope creep. These different sources of truth make it much more difficult to track down visual bugs and stylesheets with different rulesets eventually spill into each other.
Tailwind provides an answer to wrangle these values into a single file, or even a single JSON object. By giving ourselves a larger range of values to use, we can anticipate and catch possible values to avoid the naming arms race early on.
module.exports = {
theme: {
colors: {
...
primary: {
DEFAULT: '#4381DD
100: '#F0F6FF,
200: '#EBF3FF,
...
900: '#082C63,
},
}
}
}
This single object provides colour options for text, background, and border colours in a single property. There are also steps for opacities that remove the need to make new rulesets whenever we decide to use slightly transparent versions of a colour. For example, a background primary colour at the 200 shade with 70% opacity would be bg-primary-200/70
.
To take it even further, in an advanced application of Tailwind, you can create context-aware colour values with the tw-colors package. For example, you can make the primary colour a light green in a light-mode context while having the same “primary” value represent a dark green colour in a dark-mode context. Tailwind and this plugin take advantage of the prefers-color-scheme
CSS rule out of the box but the plugin also extends past it and allows you to make any number of themes beyond “light” and “dark”.
How might this context-switching be relevant? Maybe we’re working with a very large brand that has sub-brands that use different branding colours. The colours changed but we still want the brand to feel consistent and not recreate the same components. Take this example of a card:
![Design system card example](https://www.nearform.com/media/pages/digital-community/scaling-a-design-system-with-tailwind-css/5df202deae-1738756457/blog-scaling-a-design-system-with-tailwind-css-card-example-1600x.png)
It uses three colour shade values, (0
being the closest to white and 1000
being the closest to black). Maybe, in this example, there’s a large list of them in the main branding with this blue colour scheme:
![Nine design system card examples](https://www.nearform.com/media/pages/digital-community/scaling-a-design-system-with-tailwind-css/7c404d59da-1738756679/blog-scaling-a-design-system-with-tailwind-css-multiple-cards-example-1600x.png)
However, the company just spun off into a purple sub-brand! We want to consume the same components from the design system but apply the purple as what a “primary” colour represents. Instead of specifying that the cards have a blue-50
background, we can instead use a more generic term like primary-50
. Then, by using tw-colors, we can either apply a class to the <body>
tag itself or wrap all the content with a specific class or data-theme attribute and switch out the colour in which primary
means.
![Nine design system card examples in rows of three-by-three](https://www.nearform.com/media/pages/digital-community/scaling-a-design-system-with-tailwind-css/da507aaba9-1738756815/blog-scaling-a-design-system-with-tailwind-css-multiple-cards-example-purple-1600x.png)
In this case, primary
is now referring to a purple shade rather than blue.
This colouring scheme allows us to make one-offs that will inevitably arise. Let’s say, for example, we want to promote this sub-brand in the normal blue list of cards. We can do that by adding the same class or data-theme
to just the individual card and it will have its own context of theming.
![Eight design system title card examples and one design system promote the sub-brand card example](https://www.nearform.com/media/pages/digital-community/scaling-a-design-system-with-tailwind-css/f46559c535-1738756977/blog-scaling-a-design-system-with-tailwind-css-multiple-cards-example-purple-and-blue-1600x.png)
This idea of context-aware colour switching and design modes match closely with Figma’s modes for variables. Using Tailwind in conjunction with tw-colors creates a shared language between design and development and will thus reduce the amount of time between creating artboards and shipping code.
Typography and spacing in Tailwind CSS
In an extremely similar branch as expanding colour values, I’ve seen typography and text styles start off well-intentioned and structurally defined, only for the system to fall apart as applications use one-off values in the name of differentiating design. This isn’t to say that we shouldn’t have the option to do that, but we should have a system that encourages common values when they’re used and be extendable when we need one-offs.
Out of the box, Tailwind has t-shirt sizing (sm, md, lg, etc.) that allow you to override what these values represent. When we inevitably need to have different sizes or maybe a specific text size at a breakpoint, we can use a number of methods to break out of the box. By default, Tailwind’s t-shirt sizing has set values for the font size and line height attributes. For example, text-sm
represents 0.875rem (14px) for its font size and 1.25rem (20px) for its line height.
For a one-off size change for a specific breakpoint, we can use the built-in media query and use arbitrary values. For example, if we want the text size to be 1.125rem on mobile and tablet but 100px on desktop, we can use the classes:
text-lg lg:text-[100px]
To make a custom text size that changes based on breakpoints, we can make use of the @apply directive in the components layer and define a class in the global CSS file with:
@layer components {
.custom-text-lg {
@apply text-base md:text-lg lg:text-xl;
}
}
![Typography and spacing in Tailwind CSS example](https://www.nearform.com/media/pages/digital-community/scaling-a-design-system-with-tailwind-css/2d8bac2621-1738757934/blog-scaling-a-design-system-with-tailwind-css-group-1600x.png)
For even more advanced usages, you can add custom components programmatically with the Tailwind plugin API.
Colours and typography are just two of the more commonly extended classes I’ve seen, but they’re really just the tip of the iceberg. Tailwind provides an abundance of various utility classes to really customise a design system.
Putting it together on Figma’s end
Once we have the Tailwind rules in place, we can translate them into Figma. There is a use case for creating the colour and sizing values in Figma first, but it can also be useful for the designer to know what is available to use before creating designs. In this guide, we can apply the steps in the same order as we did for the Tailwind configuration, starting with colour.
Colours
Click on the background of the Figma canvas to deselect all elements and there should be a "Local Variables" option in the sidebar:
![Design system Figma local variables](https://www.nearform.com/media/pages/digital-community/scaling-a-design-system-with-tailwind-css/ae0c8bc049-1738758109/blog-scaling-a-design-system-with-tailwind-css-local-variables-1600x.png)
Create a table of colours with shading values as the rows and the different themes as the columns:
![Design system Figma table of colours](https://www.nearform.com/media/pages/digital-community/scaling-a-design-system-with-tailwind-css/aa7ab3083e-1738758229/blog-scaling-a-design-system-with-tailwind-css-table-of-colours-1600x.png)
We can then apply these colour values to fills and outlines by clicking on the “Apply styles and variables” button next to the colour option:
![Design system Figma apply styles and variables](https://www.nearform.com/media/pages/digital-community/scaling-a-design-system-with-tailwind-css/86fc2a869a-1738758332/blog-scaling-a-design-system-with-tailwind-css-table-of-colours-apply-styles-and-variables-1600x.png)
On the Tailwind end of things, install the tw-colors package and add to the plugins array in the tailwind.config.js file with the new themes:
import { createThemes } from "tw-colors";
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [
createThemes({
blue: {
50: "#F0F6FF",
200: "#DEEBFF",
400: "#96BFFF",
500: "#4381DD",
600: "#1D61C7",
950: "#082C63",
},
purple: {
50: "#F8F1FF",
200: "#E6D3FA",
400: "#D2B6EE",
500: "#9C57E2",
600: "#721DC7",
950: "#33085D",
},
}),
],
};
Numerical values
For typography and spacing (padding, margin and gap), we would add three breakpoints to represent Tailwind’s t-shirt screen sizes (no modifier, md and lg). On Figma’s end, these variables would look like this:
![Design system Figma t-shirt variables example](https://www.nearform.com/media/pages/digital-community/scaling-a-design-system-with-tailwind-css/6f9cf7ae85-1738758482/blog-scaling-a-design-system-with-tailwind-css-t-shirt-variables-example-1600x.png)
Note: You can group variables by selecting multiple variables and right-clicking to create a “New group with selection”.
This allows us to apply these variables to the padding and gap values once we set auto-layout onto the grouped components.
![Design system Figma new group with selection example](https://www.nearform.com/media/pages/digital-community/scaling-a-design-system-with-tailwind-css/9603ca6b61-1738758634/blog-scaling-a-design-system-with-tailwind-css-new-group-with-selection-example-1600x.png)
The text sizing variables are found at the bottom of the dropdown list of text sizes:
![Design system Figma text sizing variables](https://www.nearform.com/media/pages/digital-community/scaling-a-design-system-with-tailwind-css/01ee231f62-1738763466/blog-scaling-a-design-system-with-tailwind-css-text-sizing-variables-1600x.png)
In our index CSS file, we can add the following lines to match the Figma file. The typography will take advantage of the built-in font sizes but apply various sizes depending on the breakpoint. The spacing values, like gap and padding, will use numerical values.
.custom-text-base {
@apply text-sm md:text-base lg:text-lg;
}
.custom-text-title {
@apply text-xl md:text-[22px] lg:text-2xl;
}
.gap-sm {
@apply gap-2 md:gap-3 lg:gap-4;
}
.gap-md {
@apply gap-3 md:gap-4 lg:gap-5;
}
.gap-lg {
@apply gap-6 md:gap-7 lg:gap-8;
}
.p-sm {
@apply p-2 md:p-3 lg:p-4;
}
.p-md {
@apply p-3 md:p-4 lg:p-5;
}
.p-lg {
@apply p-6 md:p-7 lg:p-8;
}
Putting these values together
With these tokenised values, we can put them together to create a simple card component:
![Design system simple card component](https://www.nearform.com/media/pages/digital-community/scaling-a-design-system-with-tailwind-css/3580b48f4d-1738772232/blog-scaling-a-design-system-with-tailwind-css-simple-card-example-1600x.png)
These values will then carry over onto any artboard with a variable mode set for the breakpoint or colour theme. In this example, the artboard has its context set to "Desktop" for the numbers and has the larger text and spacing values.
![Design system artboard](https://www.nearform.com/media/pages/digital-community/scaling-a-design-system-with-tailwind-css/7f0e38fe8e-1738772349/blog-scaling-a-design-system-with-tailwind-css-artboard-1600x.png)
And as always, we can make one-off changes to elements on the code end by adding a Tailwind class to it. This removes the uncompromising rigidity of most other design systems where the engineer would have to create a new element or a workaround.
Scaling past just the design system
Once we have a single source of truth and a design system built out, we can start to use it in different applications. One of the strengths of using a design system is that it can be applied across platforms within the same branding. Tailwind exemplifies this strength by allowing its config file to consume any other Tailwind config file. For example, we can build out a design system with its own set of Tailwind rules, like having some custom-defined classes, a few official plugins and a custom-written plugin. A service that uses this design system can list the design system’s config file as a preset and inherit all of those custom rules.
This preset functionality is especially useful for larger brands that have smaller services with a similar design language. Any custom rules or plugins by the smaller services will be retained in addition to the boilerplate provided by the larger organisation. This system allows developers to write once and apply in multiple places without giving up flexibility for one-off situations.
Conclusion
Despite my initial reservations about its constraints and its steep learning curve, I’ve really come to like Tailwind and have seen its system work effectively. Its approach scales with the values and designs that come out of Figma in a structured, yet flexible way, which helps with managing ever-changing systems and catching visual inconsistencies. It’s a powerful tool that helps create scalable and cohesive design systems that grow with projects and businesses.
Insight, imagination and expertly engineered solutions to accelerate and sustain progress.
Contact