DEV Community

Cover image for Why I moved from Styled Components to (S)CSS modules
PuruVJ
PuruVJ

Posted on • Originally published at puruvj.dev

Why I moved from Styled Components to (S)CSS modules

Read in light, dark or mid-day theme

Artwork by Lucas Benjamin

This blog post will be about my reasons to move from Styled Components to SCSS Modules. Its gonna be a raw and non-technical one (i.e., you probably won't learn anything new from it).

What is styled-components?

Styled Components is a radical, new way of writing CSS for your React components. You can simply create components out of your styles

export const Main = () => {
  return <HelloWorld>Hello World</HelloWorld>;
};

const HelloWorld = styled.h1`
  font-weight: 700;
  line-height: 1.618;
`;
Enter fullscreen mode Exit fullscreen mode

This is a very, very convenient way of writing CSS. All your CSS lives in the same file as your main logic. It's ultimate Colocation. Plus if you're a freak about small components, this really enforces you to write small components, cuz the components grow large very quickly thanks to all 3 techs in a single file: HTML + CSS + TS(Yes, I'm one of those people who breath TypeScript ๐Ÿ˜‹). So you kinda feel obligated to break your components into smaller pieces, which is ultimately good. Modularity is paramount.

Hail modularity

Its just like Svelte and Vue's SFCs. They figured it out correctly, while this makes me mad about React.

Anyways, rant aside, this way of writing styles is really good, I can't insist enough. Need dynamic prop based styles? No worries, just pass props over to your styled component, and use it in there

export const Main = () => {
  return <HelloWorld weight={600}>Hello World</HelloWorld>;
};

const HelloWorld = styled.h1<{ weight: number }>`
  font-weight: ${({ weight }) => weight};
  line-height: 1.618;
`;
Enter fullscreen mode Exit fullscreen mode

Pardon the TypeScript code if you're not familiar with it or hate it. It can't be helped. Its part of my very being now ๐Ÿ˜‡.

And yes, it automatically does the scoping and vendor prefixing. vendor prefixing is runtime generated, that is it determines if the browser needs vendor prefixes, then it will churn out styles with vendor prefixes. Its like a sweet runtime PostCSS and Autoprefixer running in the browser.

It makes stuff very very easy. But this is where it starts to go wrong if you don't fully understand how React and its rendering processes work.

Here be dragons

What are CSS Modules?

CSS Modules are a slightly-less radical way of writing CSS. Its basically separate CSS files, but only modular. Syntax remains the same mostly, but it's scoped to the components where it is used (By mangling class names). The general pattern of these is this:

|-HelloWorld
  |-HelloWorld.tsx
  |-HelloWorld.module.css
Enter fullscreen mode Exit fullscreen mode

Notice I use .css at the end. It could be .scss or .less or .styl too, you name it. I personally use SCSS modules.

Notice that our CSS Module has in the name itself that its a module, *.module.*. Its a Convention over Configuration approach, very prevalent in futuristic bundlers like ESBuild, Vite, Snowpack, etc.

And to use them, you import the css file in JS, and refer to it like this.

import css from './HelloWorld.module.css';

export const Main = () => {
  return <h1 className={css.helloWorld}>Hello World</h1>;
};
Enter fullscreen mode Exit fullscreen mode

Meanwhile our CSS file:

/* HelloWorld.module.css */

.helloWorld {
  font-weight: 700;
  line-height: 1.618;
}
Enter fullscreen mode Exit fullscreen mode

The generated CSS is something like this:

/* HelloWorld.module.css */

.__B56BLAH_helloWorld_4269BRUHBRUH {
  font-weight: 700;
  line-height: 1.618;
}
Enter fullscreen mode Exit fullscreen mode

The className is mangled, and the value is substituted in place of css.helloWorld in our component.

Alright I took some artistic liberty and added some weird Elon Musk-y stuff there. The actual mangled output would be much smaller and sane ๐Ÿ˜.

CSS modules are very handy for this. Plus you can add tooling like autoprefixer to add vendor prefixes, compile stuff back to old CSS for browser compatibility.

The app in question

Now the intro is over, let's look at the app which I moved from Styled components to CSS modules. Let me introduce you to my baby, macos.now.sh, a macOS Big Sur clone written In Preact, TypeScript and uses Vite as the bundler. Check it out, I think you'll like it (Tip: Just hover over the app dock at the bottom).

Anyways, this whole app was written in Styled Components, until I threw it out of the 30+ components in favour of CSS Modules.

Why? ๐Ÿง

The simple answer ๐Ÿ‘‡

Why? Why shouldn't I not use CSS Modules

Just kidding ๐Ÿ˜…. Here's a full technical explantion ๐Ÿ‘‡

CSS not minified

Take a look at this image ๐Ÿ‘‡

Unmodified Styled Components

This the main production bundle of the app. As you can see, it's minified in some place, and not, in other places. You can see the unminified part is the CSS part. These are the styles I wrote as template literals(Or string literals, I mix both up ๐Ÿ˜…). As these aren't CSS to bundler's internal CSS minifier, it stays as it is, which is kinda bummer. I am a die-hard performance freak, and the 1st rule of performance on Web: Bundle and minify your resources. Make them as small as possible, then make them even smaller ยฏ\_(ใƒ„)_/ยฏ.

Seriously, you can check this file out right here: https://macos-web-fwyxhwxry-puruvj.vercel.app/assets/index.da0c587c.js

Why not use the babel plugin? ๐Ÿคจ

If you don't know, Styled Components has a Babel plugin for this purpose exactly, minifying the CSS inside the template literals, and its pretty decent.

But it wasn't working for me.

No literally, it wasn't working for me, as in I set up the babel plugin and did the correct config, installed the plugin, but no it wasn't working. Something was going wrong with Vite's plugin running. The plugin was working, as build times had increased a lot from before, but the output was still not minified. The same plugin worked perfectly in a create-react-app reproduction I created to check this.

But anyways, even if this problem was solved, there's a bigger Elephant in the room

CSS injected by JS

All of this CSS still lives in the JavaScript, and is only applied when JS is evaluated by the browser, and I'm pretty sure you know of this, JavaScript is HEAVY!!!. It takes quite some CPU power to parse it, and it's heavy on main thread. Our HTML being rendered by JS itself is pushing the limit, but rendering CSS using JS too? That's way too much load on the browser.

Browsers have become amazingly efficient at parsing JS as well as rendering HTML and CSS, all in parallel. But JavaScript doing all the work, well, browsers still aren't that efficient at it(For good reason).

If you want ultimate performance, CSS in separate files or inlined in style tag is the way too go. It doesn't get better than that.

Performance had become important

When I started this project almost 6 months ago (November 2020), I made myself a little deal: Do not stress about the performance. Off course, at that time, Performance meant just lower bundle sizes, not runtime performance, cuz I really had never run into any runtime perf issues before. But this project is different in the sense that there's a lot going on. There are loads of requestAnimationFrames, tons of component, lot of global state, and what not going on. And all of it is on the screen at once. You can't really lazy load much stuff, cuz almost everything is eagerly loaded.

All of it was weighing down the runtime perf of the app. The dock animation was janky, the menus took a while to open up, theme switching was also noticeably janky. So I had to finally consider the runtime performance. And the most obvious choice was to start throwing out fancy stuff and move back to basics again.

Did it work?

Absolutely!! The performance increased like crazy. Both runtime as well as bundle size.

This is the compressed CSS file output. Its run through Autoprefixer for vendor styles, and Vite automatically puts it into a single CSS file, super compressed. Take a look yourself ๐Ÿ‘‡

Optimized CSS Module file

And here's the index.js ๐Ÿ‘‡

Optimised JavaScript

Completely minified, JS lives in js files, CSS lives in its own file, and its all processed parallelly by the browser, styles aren't generated for every prop change like in styled components. Only the classes are applied, and the styles for that are already present in the CSS file. Just like the old times, simply and fast.

Perfection

Reduced Bundle Size

This total maneuver took away 60KB from my bundles, which is just huge. I removed styled-components, react-is(Styled components require it for some reason), styled-reset and color2k(For color manipulation).

If you have been coding for some time, you'll know how incredibly satisfying deleting old stuff is. ๐Ÿ˜Œ

What did it cost?

Gamora: What did it cost?; Thanos: Everything

Yup. I lost something: A great API design.

Writing styles in Styled Components is a pleasure. The API design is amazing, and I prefer it over CSS modules in terms of writing the code.

If you aren't using a style, means you aren't using a component, so the component will be called out by VSCode as not being used, so you can easily remove it. No more dead styles!!

Plus, compare the component below in Styled Components:

interface ActionCenterSurfaceProps {
  grid: [[number, number], [number, number]];
  children: ComponentChildren;
}

export const ActionCenterSurface = ({ grid, children }: ActionCenterSurfaceProps) => {
  const [[columnStart, columnSpan], [rowStart, rowSpan]] = grid;
  const [theme] = useTheme();

  return (
    <Container
      columnSpan={columnSpan}
      columnStart={columnStart}
      rowSpan={rowSpan}
      rowStart={rowStart}
      theme={theme}
    >
      {children}
    </Container>
  );
};

type ContainerProps = {
  columnStart: number;
  columnSpan: number;

  rowStart: number;
  rowSpan: number;

  theme: TTheme;
};

const Container = styled.section<ContainerProps>`
  display: grid;
  grid-auto-rows: 1fr;
  gap: 0.25rem;

  position: relative;

  padding: 0.5rem;

  border-radius: 0.75rem;

  background-color: hsla(${theme.colors.light.hsl}, 0.5);

  ${({ columnStart, columnSpan, rowSpan, rowStart, theme: localTheme }) => css`
    grid-column: ${columnStart} / span ${columnSpan};
    grid-row: ${rowStart} / span ${rowSpan};

    box-shadow: hsla(0, 0%, 0%, 0.3) 0px 1px 4px -1px, 0 0 0 ${localTheme === 'dark' ? 0.4 : 0}px hsla(
          ${theme.colors.dark.hsl},
          0.3
        );
  `};
`;
Enter fullscreen mode Exit fullscreen mode

This is one of my components in Styled Components before. As you can see, it accepts values that are numbers. If there were booleans, it would've been easy to make a class and apply the styles then. But here, the value can be anything.

And now look at the new CSS Module version:

Component:

interface ActionCenterSurfaceProps {
  grid: [[columnStart: number, columnSpan: number], [rowStart: number, rowSpan: number]];
  children: ComponentChildren;
}

export const ActionCenterSurface = ({ grid, children }: ActionCenterSurfaceProps) => {
  const [[columnStart, columnSpan], [rowStart, rowSpan]] = grid;
  const [theme] = useTheme();

  return (
    <section
      className={css.container}
      style={
        {
          '--column-start': columnStart,
          '--column-span': columnSpan,
          '--row-start': rowStart,
          '--row-span': rowSpan,

          '--border-size': `${theme === 'dark' ? 0.4 : 0}px`,
        } as React.CSSProperties
      }
    >
      {children}
    </section>
  );
};
Enter fullscreen mode Exit fullscreen mode

And the CSS for this component:

.container {
  display: grid;
  grid-auto-rows: 1fr;
  gap: 0.25rem;

  position: relative;

  padding: 0.5rem;

  border-radius: 0.75rem;
  box-shadow: hsla(0, 0%, 0%, 0.3) 0px 1px 4px -1px, 0 0 0 var(--border-size) hsla(
        var(--app-color-dark-hsl),
        0.3
      );

  background-color: hsla(var(--app-color-light-hsl), 0.5);

  grid-column: var(--column-start) / span var(--column-span);
  grid-row: var(--row-start) / span var(--row-span);
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the prop values are passed to the CSS using CSS variables. This method is good too, but the Styled Components method is cleaner in my opinion.

In future, I might try out libraries like Linaria which, during coding have the exact same API as styled-components, but the runtime is completely removed on build and the CSS is extracted into separate CSS files, which is super DOPE!!! ๐Ÿค“

Alright, that's it for today.

Signing off!! ๐Ÿ‘‹

Top comments (52)

Collapse
 
lynnntropy profile image
Lynn Romich

Performance is definitely important, but I feel like you kind of threw the baby out with the bathwater here. As you pointed out at the end, we have CSS-in-JS solutions now that have zero runtime performance hit, so there's no need to ditch CSS-in-JS entirely if performance is your only real concern.

Collapse
 
puruvj profile image
PuruVJ

Yup. The thing is, I tried Linaria too. But my project heavily relies on path aliases, and whatever Vite's alias uses internally, wasn't compatible with Linaria, so the babel plugin threw an error and I couldn't debug the problem.

So I decided to just go ahead and merge the CSS modules PR into the code. I prefer Styled Components API, but CSS modules isn't bad either. The joy of writing plain CSS is also really nice and fast, and the intellisense is amazing, so not really missing out on anything for this project.

Collapse
 
lynnntropy profile image
Lynn Romich

Ah, that's unfortunate. But yeah, CSS modules are cool too, I mainly just don't like them in React because I don't want to go back to writing CSS in a separate file.

Collapse
 
alekseiberezkin profile image
Aleksei Berezkin

No-runtime CSS-in-JS seems like a good compromise! Are there any cons you aware of? What comes into my mind โ€” they patch JS code which is fragile IMO.

Collapse
 
puruvj profile image
PuruVJ

I tried Linaria, but it didn't work with Vite with the aliases and all, so had to settle for CSS Modules. No regrets though, CSS modules are a great experience too :)

Collapse
 
moopet profile image
Ben Sinclair

It also cost readability. It's ok if you end up writing simple semantic markup (but then, everything's ok if you do that). If you rely on multiple nested divs with obfuscated class names then it'll be harder to debug and you'll annoy a lot of "tinkerers"... tinkerers like you and me.

It's the sort of thing people do to prevent the use of ad blockers, or stop people from re-skinning a site with user styles.

Personally, I want people to be able to change the look and feel of any page I make, because people have different needs, abilities and limitations.

Collapse
 
lynnntropy profile image
Lynn Romich

Not sure what you mean here, the thing with the class names getting garbled is something every styling solution (that supports scoped styles) does, because it's the only way to automatically avoid class name collisions without resorting to something like shadow DOM.

Unfortunately, the only way to avoid this is either to go back to unscoped CSS with something like BEM to avoid collisions, or to manually add stable identifiers to your components (for UI tests, supporting user styles, etc.), as either class names or data attributes.

Collapse
 
puruvj profile image
PuruVJ

I think he means it in relative terms. You can still make sense of CSS modules scoped styles, they're just same names with some junk appended here and there, where with styled components, there's no chance you could ever guess what this class stands for

Thread Thread
 
moopet profile image
Ben Sinclair

I mean it in "why not go back to regular CSS" terms.

Unfortunately, the only way to avoid this is [...]

... to do things the way they were designed to be done? I don't see this as so much of an unfortunate reality as I do a code smell.

Thread Thread
 
lynnntropy profile image
Lynn Romich

I mean it in "why not go back to regular CSS" terms.

Because "regular" (unscoped) CSS is incredibly cumbersome for large projects, and it gets even worse if you add more devs to the team. BEM and other CSS methodologies took us some of the way towards fixing this, but they're cumbersome in their own right, and only help so much. CSS-in-JS and CSS modules are the first solutions we've come up with that make CSS work well at the scales often we write it today.

... to do things the way they were designed to be done?

CSS (and most web technologies, for that matter) was never designed to be used for many of the things we use it for today. Using everything as it was originally designed isn't really compatible with modern webdev.

Thread Thread
 
puruvj profile image
PuruVJ

๐Ÿ™Œ๐Ÿ™Œ๐Ÿ™Œ๐Ÿ™Œ๐Ÿ™Œ

Collapse
 
puruvj profile image
PuruVJ

Aye!

Collapse
 
pracoon profile image
Prasham Ashesh

JS has had enough tbh. It is generating the HTML, doing the CSS, minifying stuff, optimizing stuff. the least we can do is take CSS out of its plate ๐Ÿ˜…
Good read ๐Ÿ‘

Collapse
 
puruvj profile image
PuruVJ

Yup. The whole essense of this article is your comment

(I should I've just written your commentas the whole article. Would've saved so much time and energy ๐Ÿ˜‚)

Collapse
 
astrit profile image
Astrit

In few months you are going to write another article how I moved from SCSS to pure CSS ๐Ÿ˜‚

Nonetheless great article and thorough explanation, thanks ๐Ÿ™

Collapse
 
puruvj profile image
PuruVJ

I might. Who knows ๐Ÿ˜‰๐Ÿ˜‰!! I only use SCSS for nesting, but with CSS modules even that isn't needed much, as the files are less than 60 lines big and most don't have any nesting in them

Though Vite has such a first class support for SCSS that moving to plain CSS won't be any hassle. I could do the whole thing in 2 hours :P

Collapse
 
hypejunction profile image
Ismayil Khayredinov

You can use PostCSS to deal with nesting and do a lot more

Collapse
 
boris1988 profile image
Boris

Why not use TailwindCSS with JIT compilation mode ? You write all styles inside component and final size of the css bundle is minimal

Collapse
 
puruvj profile image
PuruVJ

Well, a few points here:

  1. I like to keep my HTML and CSS separate, as in not fill it up with huge class names and all
  2. JIT wasn't there when I wrote this article ๐Ÿ˜‹
  3. This project's theming heavily relies on CSS variables, so having some styles as Tailwind classes in Html and rest of css vars theming stuff in the actual CSS would've been really bad for peace of mind
Collapse
 
boris1988 profile image
Boris

I'm asking what will you use if you will start a new project ? Tailwind has Intellisense class sorting plugins, this makes actual writing of text much shorter. You just type 1-2 letters and ther press TAB or use arrows to choose. Also it is much easier for other people to edit your code, they don't need to understand the whole structure of styles, everything is global.

Thread Thread
 
puruvj profile image
PuruVJ

I would still go for plain CSS Modules, because I use CSS vars extensively, and having some styles inline, and the variables styles in a stylesheet wouldn't be smart

Thread Thread
 
boris1988 profile image
Boris

I think if you try Tailwind in a big project, you will realize soon that it saves a lot of time that you don't need to switch between files. All styling inside component and no need for additional files

Thread Thread
 
puruvj profile image
PuruVJ

Oh I have used Tailwind on some medium sized projects. Coming back to Styled Components and now plain CSS once again is the next in progression.

Collapse
 
sebastienlorber profile image
Sebastien Lorber

Author of CSS modules is working on Vanilla Extract, that could be a nice alternative

github.com/seek-oss/vanilla-extract

Collapse
 
puruvj profile image
PuruVJ

yeah, Mark is killing it. This project is amazing really. Only I wish its api was like styled components. Writing styles in JS objects isn't fun, in terms of intellisense ๐Ÿ˜”๐Ÿ˜”

Collapse
 
alphavader profile image
KB

I agree in the performance side.. My experience is, when the project is getting really really big, styled components are a lot better to maintain.

Collapse
 
puruvj profile image
PuruVJ

Definitely, if its a team. For this project, its just me(Well, now there are more thankfully :) ), so maintainability isn't a huge problem. Plus I force myself to keep my components less than 60 lines(Roughly, not a hard limit), so the CSS is smaller too, so its automatically maintainable. Plus the CSS variables theming works pretty well, so that doesn't get in the way too ๐Ÿ˜‰

Collapse
 
bryanwaddington profile image
Bryan Waddington

My experience is, when the project is getting really really big, styled components are a lot better to maintain.

Please could you explain why that is, thanks.

Collapse
 
quintisimo profile image
Quintus Cardozo

Since you live typescript like me your should give this plug-in a go: npmjs.com/package/typescript-plugi...

It's adds some nice intellisense when working with CSS modules

Collapse
 
puruvj profile image
PuruVJ

I put it in the workflow. Its amazing. can't believe I didn't know about this before github.com/PuruVJ/macos-web/commit...

Collapse
 
puruvj profile image
PuruVJ

OMG!!! This is marvellous!!!!! I can finally get rid of that extension

Collapse
 
myogeshchavan97 profile image
Yogesh Chavan

I also prefer CSS modules. If someone want an introduction to CSS Modules, check out this article.

Collapse
 
ogranada profile image
Andres Granada

I liked your post, it is great to know that Iโ€™m not the only person concerned about the cost of css in JS. About readability and in order to avoid prefixes to isolate the components you can use a CSS architecture, like bliss github.com/gilbox/css-bliss .

Collapse
 
puruvj profile image
PuruVJ

Hmm it seems nice, but its a convention in the end, like BEM. Another thing to remember while coding. The reason I turned to Styled Components and CSS Modules is to not think about this stuff, just put some dumb selectors at place and expect nothing to break.

I don't do well with CSS based conventions. never have ๐Ÿ˜