Solving the design-development drift
One of the key challenges when working across design and engineering teams is ensuring that the designs created by the design team are implemented accurately by the engineering team. It's a recurring pain point I've encountered at nearly every organisation I've worked with.
The relatively recent emergence of design systems has helped bridge this gap. In my experience, they are often approached as a design-first problem. The design team sets up the system within Figma and uses it to produce mockups that are then handed over to the engineering team for implementation.
While this approach helps drive consistency at the design level, it doesn't solve the problem of ensuring these designs – no matter how consistent or polished they are – are properly translated into code.
The fundamental challenge is that you end up with two sources of truth; a Figma design library, and a component library in code. These need to be kept aligned, or else, the consistency a design system aims to provide gradually breaks down.
In this guide, I'll outline a practical approach to addressing this problem.
Objectives
Before diving into solutions, let's clearly define what we're trying to achieve.
- Ensure that design system components and tokens are used and implemented consistently across both Figma and frontend code.
- Depending on the project, enable these components and tokens to be shareable across multiple codebases.
Sharing design tokens
In Figma, design tokens are stored as variables within a design library file.
In the codebase, they are usually defined as CSS variables, themselves either hardcoded or generated from JSON.
To solve our problem, we need a tool that allows us to define our tokens in a centralised location, then access and use them our in Figma, and our codebase. Options, include Tokens Studio and Specify.
Sharing components
Creating shared components is a more complex challenge, and I think the approach to take ultimately comes down to one fundamental question: should components be code-first or design-first? The answer to that question lies within the answer to another; is it easier to convert design to code, or code to design?
Converting a design into production-ready code is a manual and lengthy process. You need to account for edge cases, interactive states, cross-browser testing, accessibility, composability, and integration with the broader architecture.
While there are tools that can export HTML, CSS, or even JSX from a Figma frame, the generated code always requires adjustments to account for whatever conventions exist in your code that the generating tool will have no awareness of, and won't be able to replicate.
On the other hand, tools like story.to.design and Anima allow components to be pulled directly into Figma from Storybook or Histoire stories, with minimal setup time – all you need is the relevant document.
Some designers may not like this idea, fearing that it limits their creative freedom or control. This is understandable, but in practice, I don't think it does. The code-first approach simply ensures that once the design language is agreed upon, the code becomes the canonical source of truth. Regardless of the approach taken, developers will still need to update the code. It also doesn't prevent designers from experimenting or exploring new directions in Figma – I will provide a practical example in the next section.
With Figma
First, you will need to create a Storybook or Histoire document. This should live wherever your components live. Personally, I like using Storybook. You can host it using Vercel, GitHub Pages, or Chromatic. Chromatic is a good idea because it also provides automated visual regression testing.
Inside Figma, you can create a standalone library file that pulls components from Storybook using a plugin such as story.to.design or Anima. And that's basically it! The library file can then be published and consumed by feature files in Figma, just like a regular library would. Whenever components in the Storybook are updated, you can resync them in Figma.
In the event that a designer wants to test how a change might look across the system - for example, switching buttons from rounded to square corners - they can make that change in the Figma library file and re-publish. The change can then either be reverted manually or through the plugin interface (story.to.design allows you to reset the instance). If the designer decides the change should be rolled out, they can simply request for a developer to update the corresponding component in code, after which the new version can be resynced from Storybook.
With other codebases
Going back to our objectives, it isn't uncommon for organisations to want to create multiple frontends that should all look consistent. It may not always be possible to create a single component base, as different codebases may use different frameworks – for example, React and React Native.
However, in cases where the components can be shared, there are generally two approaches:
- Publish them as a dependency either through NPM or a platform like bit.dev
- Setup a monorepo architecture, which allows you to share the code directly via tools like Turborepo or Nx.
Enforcing standards
Design standards
Enforcement here relies heavily on workflow. Designers should adhere to the shared Figma libraries and use the established tokens. Plugins like Design Lint can help flag inconsistencies, though unfortunately, not in a fully automated way.
Development standards
Linters can help here too. For example, if working with Tailwind, you can use the eslint-plugin-tailwind to block unapproved patterns. This, however, doesn't enforce adherence to broader authoring guidelines.
I am currently exploring using Codex as part of a GitHub action to automatically review pull requests and verify that code standards are followed.
Out of the box solutions like CodeRabbit may also be able to do this. The idea would be to provide a clear set of guidelines which the bot can use to check all required standards are being met. I will write a standalone piece about this in due course.
Preventing regressions
On the design side, this isn't really an issue as there is no two-way sync provided by the tools we've considered herein. Although designers can override whatever is pulled in from Storybook, they can't publish to it.
On the code side, developers could make changes to global components that could prove problematic. This becomes all the more dangerous if the component is used across multiple frontends.
For this reason, it pays to ensure the component is authored properly from the outset. A standalone post will be published on this shortly.
To prevent unexpected changes, it may also be a good idea to setup GitHub CODEOWNERS, which protects core files by requiring explicit approval for edits to system-critical components.
Additionally, Chromatic can be incorporated into the CI pipeline to help prevent visual regressions.