Back to blog

How we migrated 541 components from Styled Components to Emotion with zero bugs

A case study of how we dogfooded our visual testing tool to streamline a CSS refactor

loading
Varun Vachhar
@winkerVSbecks
Last updated:

Refactoring CSS is one of the most challenging tasks as a frontend developer. You need to improve the code without altering the look and feel of the UI.

But it's tricky to catch visual changes as you refactor an entire production codebase across multiple repos. Moreover, dealing with global styles, overrides, pseudo-states and browser quirks makes the job extra complicated.

Last quarter we migrated from Styled Components to Emotion across all our codebases. That meant refactoring 541 components across five codebases and not breaking any UI along the way. We couldn't have done it without automated tests. Read on to learn about the migration and our test setup.

Wait, but why?

Recently, our team was at a crossroads. For historical reasons, Storybook and Chromatic were built with different styling libraries. Storybook used Emotion. Chromatic, which came later, used Styled Components, as did the design system and all subsequent sites.

Over the past year, the Storybook team launched many features that also introduced new UI patterns. Maintaining parity between two versions of the same components slowed us down and added more work, so it was time to invest in a unified styling strategy.

We considered porting Storybook to use Styled Components, but that would lead to an unnecessary upgrade cycle for users and open the door for dependency headaches. The team felt that Emotion offered better performance and features for our use case so we decided to refactor everything else to use Emotion.

541 components across five codebases needed to be updated! To lead the migration, we enlisted Mateusz Burzyński, one of the maintainers of Emotion.

CSS refactors lead to accidental bugs

CSS is contextual. The styling of a component changes based on the UI and the application state. Take, for example, this Cardinal component. It has multiple variations, which all look slightly different.

As you refactor the CSS, you have to ensure that each variant appears the same as before. But simulating these states for 500+ components is hard to eyeball and extremely tedious.

The cascading nature of CSS means that minor changes in one file can have unintended consequences without warnings or feedback. Components often override the styles for their child components and global styling has unpredictable impacts on UI elements.

Manually checking every components' visual appearance during the refactor would take more time than implementing the actual changes. We needed a quick and automatic mechanism to compare the appearance of all components to their previous versions.

Visual tests to the rescue!

For logic-based code, developers rely on unit tests. If all tests pass, you can assume that the rewritten function works the same as before. Chromatic's visual testing service offers the same security for UI components. It automatically checks the UI for visible changes using the cloud.

As maintainers of Storybook, we practice what we preach. We use a component-driven approach to building interfaces and capture component states as stories—for all levels of components, atoms to pages. We have over 2,300 stories!

Stories for the Chromatic settings page

Chromatic captures an image snapshot of every story—as it appears in the browser. Every commit, a new snapshot is captured and compared to the previous one to identify visual differences. You then review the visual changes to decide if they’re intentional updates or accidental bugs.

Our strategy for swapping out Styled Components for Emotion was straightforward: switch component styles then run visual tests. There should be zero changes found.

From Styled Components to Emotion

With a visual testing strategy in place, let's talk about the actual code changes. The good news was that Emotion and Styled Components have converged on a common API. That means most changes were just swapping out the import statement:

-import styled from 'styled-components';
+import styled from '@emotion/styled';
 
const MyComponent = styled.div`...`;

To keep all the codebases in sync, we don’t use Emotion directly and instead export it via the @storybook/theming package:

import { css, Global } from '@storybook/theming';

That said, there are some key differences. For starters, Emotion doesn't support attrs, which is useful when you want every instance of a component to have that prop. For example:

const Arrow = styled(Icon).attrs({ icon: 'arrowdown' })``;

The workaround is to either use defaultProps or add in a wrapper component, like so:

const Arrow = styled((props: Omit<ComponentProps<typeof Icon>, 'icon'>) => (
 <Icon {...props} icon="arrowdown" />
))``;

The SSR setup is also quite different for the two libraries. Emotion offers zero-config SSR, which worked for most of our sites. However, this default setup doesn't work correctly with components that use nth-child or similar selectors. We had to switch to the explicit manual SSR setup for those cases.

// gatsby-ssr.js
import React from 'react';
import { renderToString } from 'react-dom/server';
import { CacheProvider } from '@storybook/theming';
import createCache from '@emotion/cache';
import createEmotionServer from 'create-emotion-server';
 
const EMOTION_KEY = 'chr';
 
export const replaceRenderer = ({
 setHeadComponents,
 replaceBodyHTMLString,
 bodyComponent,
}) => {
 const cache = createCache({ key: EMOTION_KEY });
 cache.compat = true;
 
 const { extractCritical } = createEmotionServer(cache);
 const { ids, css, html } = extractCritical(
   renderToString(<CacheProvider value={cache}>{bodyComponent}</CacheProvider>)
 );
 
 setHeadComponents([
   <style
     {...{
       [`data-emotion-${EMOTION_KEY}`]: ids.join(' '),
       dangerouslySetInnerHTML: { __html: css },
     }}
   />,
 ]);
 
 replaceBodyHTMLString(html);
};

Wrapping up

The visual testing strategy helped Mateusz and the team spot bugs sooner without having to check every story manually. Most of the bugs were caused by same-specificity rules, which were fixed using the && override. Once all tests passed, we could merge with confidence!

https://github.com/storybookjs/design-system/pull/305/files
"I can not imagine working on such a migration without a visual regression test suite. Chromatic made it super easy to review changes and to ensure that the quality of the product stays high as ever."

— Mateusz Burzyński

Join the Storybook mailing list

Get the latest news, updates and releases

6,615 developers and counting

We’re hiring!

Join the team behind Storybook and Chromatic. Build tools that are used in production by 100s of thousands of developers. Remote-first.

View jobs

Popular posts

Figma plugin sneak peek

Storybook Connect links your stories to Figma components
loading
Dominic Nguyen

Structuring your Storybook

Best practices for organizing components and stories
loading
Varun Vachhar

Design integrations for Storybook

How to integrate design tools with Storybook
loading
Dominic Nguyen
Join the community
6,615 developers and counting
WhyWhy StorybookComponent-driven UI
DocsGuidesTutorialsChangelogTelemetry
CommunityAddonsGet involvedBlog
ShowcaseExploreProjectsComponent glossary
Open source software
Storybook

Maintained by
Chromatic
Special thanks to Netlify and CircleCI