DEV Community 👩‍💻👨‍💻

Sam Magura
Sam Magura

Posted on • Updated on

Why We're Breaking Up with CSS-in-JS

Hi, I'm Sam — software engineer at Spot and the 2nd most active maintainer of Emotion, a widely-popular CSS-in-JS library for React. This post will delve into what originally attracted me to CSS-in-JS, and why I (along with the rest of the Spot team) have decided to shift away from it.

We'll start with an overview of CSS-in-JS and give an overview of its pros & cons. Then, we'll do a deep dive into the performance issues that CSS-in-JS caused at Spot and how you can avoid them.

What is CSS-in-JS?

As the name suggests, CSS-in-JS allows you to style your React components by writing CSS directly in your JavaScript or TypeScript code:

// @emotion/react (css prop), with object styles
function ErrorMessage({ children }) {
  return (
    <div
      css={{
        color: 'red',
        fontWeight: 'bold',
      }}
    >
      {children}
    </div>
  );
}

// styled-components or @emotion/styled, with string styles
const ErrorMessage = styled.div`
  color: red;
  font-weight: bold;
`;
Enter fullscreen mode Exit fullscreen mode

styled-components and Emotion are the most popular CSS-in-JS libraries in the React community. While I have only used Emotion, I believe virtually all points in this article apply to styled-components as well.

This article focuses on runtime CSS-in-JS, a category which includes both styled-components and Emotion. Runtime CSS-in-JS simply means that the library interprets and applies your styles when the application runs. We'll briefly discuss compile-time CSS-in-JS at the end of the article.

The Good, The Bad, and the Ugly of CSS-in-JS

Before we get into the nitty-gritty of specific CSS-in-JS coding patterns and their implications for performance, let's start with a high-level overview of why you might choose to adopt the technology, and why you might not.

The Good

1. Locally-scoped styles. When writing plain CSS, it's very easy to accidentally apply styles more widely than you intended. For example, imagine you're making a list view where each row should have some padding and a border. You'd likely write CSS like this:

   .row {
     padding: 0.5rem;
     border: 1px solid #ddd;
   }
Enter fullscreen mode Exit fullscreen mode

Several months later when you've completely forgotten about the list view, you create another component that has rows. Naturally, you set className="row" on these elements. Now the new component's rows have an unsightly border and you have no idea why! While this type of problem can be solved by using longer class names or more specific selectors, it's still on you as the developer to ensure there are no class name conflicts.

CSS-in-JS completely solves this problem by making styles locally-scoped by default. If you were to write your list view row as

<div css={{ padding: '0.5rem', border: '1px solid #ddd' }}>...</div>
Enter fullscreen mode Exit fullscreen mode

there is no way the padding and border can accidentally get applied to unrelated elements.

Note: CSS Modules also provide locally-scoped styles.

2. Colocation. If using plain CSS, you might put all of your .css files in a src/styles directory, while all of your React components live in src/components. As the size of the application grows, it quickly becomes difficult to tell which styles are used by each component. Often times, you will end up with dead code in your CSS because there's no easy way to tell that the styles aren't being used.

A better approach for organizing your code is to include everything related to a single component in same place. This practice, called colocation, has been covered in an excellent blog post by Kent C. Dodds.

The problem is that it's hard to implement colocation when using plain CSS, since CSS and JavaScript have to go in separate files, and your styles will apply globally regardless of where the .css file is located. On the other hand, if you're using CSS-in-JS, you can write your styles directly inside the React component that uses them! If done correctly, this greatly improves the maintainability of your application.

Note: CSS Modules also allow you to colocate styles with components, though not in the same file.

3. You can use JavaScript variables in styles. CSS-in-JS enables you to reference JavaScript variables in your style rules, e.g.:

// colors.ts
export const colors = {
  primary: '#0d6efd',
  border: '#ddd',
  /* ... */
};

// MyComponent.tsx
function MyComponent({ fontSize }) {
  return (
    <p
      css={{
        color: colors.primary,
        fontSize,
        border: `1px solid ${colors.border}`,
      }}
    >
      ...
    </p>
  );
}
Enter fullscreen mode Exit fullscreen mode

As this example shows, you can use both JavaScript constants (e.g. colors) and React props / state (e.g. fontSize) in CSS-in-JS styles. The ability to use JavaScript constants in styles reduces duplication in some cases, since the same constant does not have to be defined as both a CSS variable and a JavaScript constant. The ability to use props & state allows you to create components with highly-customizable styles, without using inline styles. (Inline styles are not ideal for performance when the same styles are applied to many elements.)

The Neutral

1. It's the hot new technology. Many web developers, myself included, are quick to adopt the hottest new trends in the JavaScript community. Part of this is rationale, since in many cases, new libraries and frameworks have proven to be massive improvements over their predecessors (just think about how much React enhances productivity over earlier libraries like jQuery). On the other hand, the other part of our obsession with shiny new tools is just that — an obsession. We're afraid of missing out on the next big thing, and we might overlook real drawbacks when deciding to adopt a new library or framework. I think this has certainly been a factor in the widespread adoption of CSS-in-JS — at least it was for me.

The Bad

1. CSS-in-JS adds runtime overhead. When your components render, the CSS-in-JS library must "serialize" your styles into plain CSS that can be inserted into the document. It's clear that this takes up extra CPU cycles, but is it enough to have a noticeable impact on the performance of your application? We'll investigate this question in depth in the next section.

2. CSS-in-JS increases your bundle size. This is an obvious one — each user who visits your site now has to download the JavaScript for the CSS-in-JS library. Emotion is 7.9 kB minzipped and styled-components is 12.7 kB. So neither library is huge, but it all adds up. (react + react-dom is 44.5 kB for comparison.)

3. CSS-in-JS clutters the React DevTools. For each element that uses the css prop, Emotion will render <EmotionCssPropInternal> and <Insertion> components. If you are using the css prop on many elements, Emotion's internal components can really clutter up the React DevTools, as seen here:

The React DevTools displaying many internal Emotion components

The Ugly

1. Frequently inserting CSS rules forces the browser to do a lot of extra work. Sebastian Markbåge, member of the React core team and the original designer of React Hooks, wrote an extremely informative discussion in the React 18 working group about how CSS-in-JS libraries would need to change to work with React 18, and about the future of runtime CSS-in-JS in general. In particular, he says:

In concurrent rendering, React can yield to the browser between renders. If you insert a new rule in a component, then React yields, the browser then have to see if those rules would apply to the existing tree. So it recalculates the style rules. Then React renders the next component, and then that component discovers a new rule and it happens again.

This effectively causes a recalculation of all CSS rules against all DOM nodes every frame while React is rendering. This is VERY slow.

Update 2022-10-25: This quote from Sebastian is specifically referring to performance in React Concurrent Mode, without useInsertionEffect. I recommend reading the full discussion if you want an in-depth understanding of this. Thanks to Dan Abramov for pointing out this inaccuracy on Twitter.

The worst thing about this problem is that it's not a fixable issue (within the context of runtime CSS-in-JS). Runtime CSS-in-JS libraries work by inserting new style rules when components render, and this is bad for performance on a fundamental level.

2. With CSS-in-JS, there's a lot more that can go wrong, especially when using SSR and/or component libraries. In the Emotion GitHub repository, we receive tons of issues that go like this:

I'm using Emotion with server-side rendering and MUI/Mantine/(another Emotion-powered component library) and it's not working because...

While the root cause varies from issue to issue, there are some common themes:

  • Multiple instances of Emotion get loaded at once. This can cause problems even if the multiple instances are all the same version of Emotion. (Example issue)
  • Component libraries often do not give you full control over the order in which styles are inserted. (Example issue)
  • Emotion's SSR support works differently between React 17 and React 18. This was necessary for compatibility with React 18's streaming SSR. (Example issue)

And believe me, these sources of complexity are just the tip of the iceberg. (If you're feeling brave, take a look at the TypeScript definitions for @emotion/styled.)

Performance Deep Dive

At this point, it's clear that there are both significant pros and significant cons to runtime CSS-in-JS. To understand why our team is moving away from the technology, we need to explore the real-world performance impact of CSS-in-JS.

This section focuses on the performance impact of Emotion, as it was used in the Spot codebase. As such, it would be a mistake to assume that the performance numbers presented below apply to your codebase as well — there are many ways to use Emotion, and each of these has its own performance characteristics.

Serialization Inside of Render vs. Outside of Render

Style serialization refers to the process by which Emotion takes your CSS string or object styles and converts them to a plain CSS string that can be inserted into the document. Emotion also computes a hash of the plain CSS during serialization — this hash is what you see in the generated class names, e.g. css-15nl2r3.

While I have not measured this, I believe one of the most significant factors in how Emotion performs is whether style serialization is performed inside or outside of the React render cycle.

The examples in the Emotion docs perform serialization inside render, like this:

function MyComponent() {
  return (
    <div
      css={{
        backgroundColor: 'blue',
        width: 100,
        height: 100,
      }}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Every time MyComponent renders, the object styles are serialized again. If MyComponent renders frequently (e.g. on every keystroke), the repeated serialization may have a high performance cost.

A more performant approach is to move the styles outside of the component, so that serialization happens one time when the module loads, instead of on each render. You can do this with the css function from @emotion/react:

const myCss = css({
  backgroundColor: 'blue',
  width: 100,
  height: 100,
});

function MyComponent() {
  return <div css={myCss} />;
}
Enter fullscreen mode Exit fullscreen mode

Of course, this prevents you from accessing props in your styles, so you are missing out on one of the main selling points of CSS-in-JS.

At Spot, we performed style serialization in render, so the following performance analysis will focus on this case.

Benchmarking the Member Browser

It's finally time to make things concrete by profiling a real component from Spot. We'll be using the Member Browser, a fairly simple list view that shows you all of the users in your team. Virtually all of the Member Browser's styles are using Emotion, specifically the css prop.

The Member Browser in Spot

For the test,

  • The Member Browser will display 20 users,
  • The React.memo around the list items will be removed, and
  • We'll force the top-most <BrowseMembers> component to render each second, and record the times for the first 10 renders.
  • React Strict Mode is off. (It effectively doubles the render times you see in the profiler.)

I profiled the page using the React DevTools and got 54.3 ms as the average of the first 10 render times.

My personal rule of thumb is that a React component should take 16 ms or less to render, since 1 frame at 60 frames per second is 16.67 ms. The Member Browser is currently over 3 times this figure, so it's a pretty heavyweight component.

This test was performed on an M1 Max CPU which is WAY faster than what the average user will have. The 54.3 ms render time that I got could easily be 200 ms on a less powerful machine.

Analyzing the Flamegraph

Here's the flamegraph for a single list item from the above test:

Performance flamegraph of the BrowseMembersItem component

As you can see, there are a huge number of <Box> and <Flex> components being rendered — these are our "style primitives" which use the css prop. While each <Box> only takes 0.1 – 0.2 ms to render, this adds up because the total number of <Box> components is massive.

Benchmarking the Member Browser, without Emotion

To see how much of this expensive render was due to Emotion, I rewrote the Member Browser styles using Sass Modules instead of Emotion. (Sass Modules are compiled to plain CSS at build time, so there is virtually no performance penalty to using them.)

I repeated the same test described above and got 27.7 ms as the average of the first 10 renders. That's a 48% decrease from the original time!

So, that's the reason we are breaking up with CSS-in-JS: the runtime performance cost is simply too high.

To repeat my disclaimer from above: this result only directly applies to the Spot codebase and how we were using Emotion. If your codebase is using Emotion in a more performant way (e.g. style serialization outside of render), you will likely see a much smaller benefit after removing CSS-in-JS from the equation.

Here is the raw data for those who are curious:

Spreadsheet showing render times between Emotion and non-Emotion Member Browser

Our New Styling System

After we made up our minds to switch away from CSS-in-JS, the obvious question is: what should we be using instead? Ideally, we want a styling system that has performance similar to that of plain CSS while keeping as many of the benefits of CSS-in-JS as possible. Here are the primary benefits of CSS-in-JS that I described in the section titled "The Good":

  1. Styles are locally-scoped.
  2. Styles are colocated with the components they apply to.
  3. You can use JavaScript variables in styles.

If you paid close attention to that section, you'll remember that I said that CSS Modules also provide locally-scoped styles and colocation. And CSS Modules compile to plain CSS files, so there is no runtime performance cost to using them.

The main downside to CSS Modules in my mind is that, at end of the day, they are still plain CSS — and plain CSS is lacking features that improve DX and reduce code duplication. While nested selectors are coming to CSS, they aren't here yet, and this feature is a huge quality of life boost for us.

Fortunately, there is an easy solution to this problem — Sass Modules, which are simply CSS Modules written in Sass. You get the locally-scoped styles of CSS Modules AND the powerful build-time features of Sass, with essentially no runtime cost. This is why Sass Modules will be our general purpose styling solution going forward.

Side note: With Sass Modules, you lose benefit 3 of CSS-in-JS (the ability to use JavaScript variables in styles). Though, you can use an :export block in your Sass file to make constants from the Sass code available to JavaScript. This isn't as convenient, but it keeps things DRY.

Utility Classes

One concern the team had about switching from Emotion to Sass Modules is that it would be less convenient to apply extremely common styles, like display: flex. Before, we would write:

<FlexH alignItems="center">...</FlexH>
Enter fullscreen mode Exit fullscreen mode

To do this using only Sass Modules, we would have to open the .module.scss file and create a class that applies the styles display: flex and align-items: center. It's not the end of the world, but it's definitely less convenient.

To improve the DX around this, we decided to bring in a utility class system. If you aren't familiar with utility classes, they are CSS classes that set a single CSS property on the element. Usually, you will combine multiple utility classes to get the desired styles. For the example above, you would write something like this:

<div className="d-flex align-items-center">...</div>
Enter fullscreen mode Exit fullscreen mode

Bootstrap and Tailwind are the most popular CSS frameworks that offer utility classes. These libraries have put a lot of design effort into their utility systems, so it made the most sense to adopt one of them instead of rolling our own. I had already been using Bootstrap for years, so we went with Bootstrap. While you can bring in the Bootstrap utility classes as a pre-built CSS file, we needed to customize the classes to fit our existing styling system, so I copied the relevant parts of the Bootstrap source code into our project.

We've been using Sass Modules and utility classes for new components for several weeks now and are quite happy with it. The DX is similar to that of Emotion, and the runtime performance is vastly superior.

Side note: We're also using the typed-scss-modules package to generate TypeScript definitions for our Sass Modules. Perhaps the largest benefit of this is that it allowed us to define a utils() helper function that works like classnames, except it only accepts valid utility class names as arguments.

A Note about Compile-Time CSS-in-JS

This article focused on runtime CSS-in-JS libraries like Emotion and styled-components. Recently, we've seen an increasing number of CSS-in-JS libraries that convert your styles to plain CSS at compile time. These include:

These libraries purport to provide a similar benefits to runtime CSS-in-JS, without the performance cost.

While I have not used any compile-time CSS-in-JS libraries myself, I still think they have drawbacks when compared with Sass Modules. Here are the drawbacks I saw when looking at Compiled in particular:

  • Styles are still inserted when a component mounts for the first time, which forces the browser to recalculate the styles on every DOM node. (This drawback was discussed in the section title "The Ugly".)
  • Dynamic styles like the color prop in this example cannot be extracted at build time, so Compiled adds the value as a CSS variable using the style prop (a.k.a. inline styles). Inline styles are known to cause suboptimal performance when applied many elements.
  • The library still inserts boilerplate components into your React tree as shown here. This will clutter up the React DevTools just like runtime CSS-in-JS.

Conclusion

Thanks for reading this deep dive into runtime CSS-in-JS. Like any technology, it has its pros and cons. Ultimately, it's up to you as a developer to evaluate these pros and cons and then make an informed decision about whether the technology is right for your use case. For us at Spot, the runtime performance cost of Emotion far outweighed the DX benefits, especially when you consider that the alternative of Sass Modules + utility classes still has a good DX while providing vastly superior performance.

About Spot

At Spot, we're building the future of remote work. When companies go remote, they often lose the sense of connection and culture that was present in the office. Spot is a next-gen communication platform that brings your team together by combining traditional messaging and video conferencing features with the ability to create & customize your own 3D virtual office. Please check us out if that sounds interesting!

P.S. We're looking for talented software engineers to join the team! See here for details.

A picture of Spot

This post was also published on the Spot blog.

Top comments (133)

Collapse
 
airtonix profile image
Zenobius Jiricek • Edited on

To be honest, the whole "it's hard to tell what's used by what" in plain CSS is already solved with BEM.

BEM scales very nicely.

So by using new CSS variables with CSS modules to create locally scoped overrides you can still create runtime themed styles.

Yes your "frontend" Devs will need to learn CSS instead of just treating it like the Typescript autocomplete object faceroll experience that current cssinjs promotes.

Other unpopular opinions I hold:

Nested CSS rules made popular by lesscss are a maintenance nightmare. Don't use them.

Extends is also bad.

Mixins... Yep I hate them too.

Collapse
 
dstaver profile image
Daniel Staver

I love BEM and it's what I use when writing regular CSS for my own use.

The problem with BEM isn't BEM itself, it's getting your coworkers, new developers
and external agencies to understand and follow the syntax. We implemented BEM at a previous workplace and it worked perfectly until some core developers quit and new ones were hired, and then the styles just quickly deteriorated.

CSS modules and CSS-in-JS are locally scoped by default, and you have to specifically add syntax to make things global. As much as I dislike Tailwind syntax, that too has the advantage of keeping styling local to the place it's applied at least.

Collapse
 
markohologram profile image
Marko A • Edited on

The problem with BEM isn't BEM itself, it's getting your coworkers, new developers and external agencies to understand and follow the syntax

This is my experience as well. BEM can scale really well, but keeping it consistent is a problem when you have multiple people working in a codebase. I mean same can be said for any tool. Getting everyone on board is usually the hardest part.

People are also detracted from BEM because of it's syntax and because it "looks ugly" to them, but once you actually start using it, your CSS becomes a breeze to use and read because its "standardized" in some way. It also produces flat styling without horrible nesting which is a big plus in my book. Simpler flat styling allows easier per situation override if needed and you don't have to battle CSS selector nesting and specificity that much at all.

When using BEM, I've usually went with BEM + utility classes approach. Some more common things like spacing (padding, margin), colors, and maybe some flex/block properties would be mostly in utility classes and everything else would be BEM. It worked quite nicely for projects that I've been working on. It also allowed me to build UI faster because I wouldn't have to apply same spacing values all over again for each component, but could just apply a utility class name to some wrapper element and get the spacing that I want.

These days when working with React, the projects that I usually work on use SASS Modules and that also scales nicely. It's easier to get people on the same page because there is no specific naming convention like BEM and people don't have to worry about naming things because everything is locally scoped. This approach works nicely in most situations except few edge cases where styles need to be shared or something kinda needs to be global, then it's not so elegant anymore and in a single file. But other than that, it works really well and I don't have to leave comments on PRs like "hey the name of this class doesn't adhere to BEM principles, it should be this and that etc."

I still prefer SASS Modules compared to CSS-in-JS (emotion, styled-components) because you are still "mostly" writing just regular CSS that is closer to the native web.

I'm also working on a project that uses styled-components and even though I do like type safety we get with that because we use Typescript, it also makes every little styling its own component and to see any conditional logic for styling I always have to jump to source of a styled component to see what's going on. At least with SASS Modules and conditionally applying classNames I can immediately see what class is being applied for a certain condition and just by its name I can probably tell what it does without having to go into its source.

But that's just me and my personal experience.

Collapse
 
baukereg profile image
Bauke Regnerus • Edited on

Did the new developers read the BEM documentation? In my experience a lot of people find that BEM "looks ugly" at first sight and then don't even try to understand it. While it solves so many problems.

Collapse
 
airtonix profile image
Zenobius Jiricek

There's linting you can use to enforce this.

Collapse
 
juliansoto profile image
Julian Soto

Sass paved the way but it's time to ditch it.

Collapse
 
jfbrennan profile image
Jordan Brennan

Haven’t used SASS or Less or CSS-in JS for at least 5 years. Custom HTML and utility classes scale better than anything and don’t depend on a framework or tooling.

Thread Thread
 
kopseng profile image
Carl-Erik Kopseng

When you say "Custom HTML", does that mean using Web Components? The term is a bit ambiguous. To make things clear, you now do

<my-foo>yo</my-foo>
Enter fullscreen mode Exit fullscreen mode

and in the implementation of my-foo utilize utility classes (like .align-center)?

Thread Thread
 
jfbrennan profile image
Jordan Brennan • Edited on

Yes and no :)

I'm about to publish a blog post (and speak) about this, but technically no they are not Web Components, they are CSS.

For just about ever, HTML would let you create random tags like <foo> and style those with CSS foo { color: red } and it all just worked.

This was pretty much universally dismissed as a bad idea, although it kind of wasn't. Microsoft even wrote a blog post about support for it in IE5 (yes, 5, that's how long we've been convincing ourselves custom HTML tags are bad!).

Anyway, with the introduction of Custom Elements there is now a "safer" way of defining custom HTML elements. And here's where this methodology comes in (I am calling it Tag, Attributes, then Classes or TAC). Because HTML and CSS have always been willing and able to style any tag, we can define custom tags following the Custom Elements style - <my-foo> - and just style them my-foo { color: red }.

This has two big benefits:

  • First, custom CSS-only components use the same declarative tag+attributes API as other HTML elements. For example, developers would write this:
<x-alert status="success">Success message.</x-alert>
Enter fullscreen mode Exit fullscreen mode

Instead of this:

<div class="alert alert--success">Success message.</div>
Enter fullscreen mode Exit fullscreen mode

Note that the only difference in the CSS for either of those components is the selectors.

  • Second, CSS-only components can evolve into JavaScript-powered components without any hacks or frameworks or build steps, and no breaking changes because TAC defines components with a Custom Element-compatible tag. So, here we have the same custom HTML and CSS component being enhanced with some vanilla JavaScript:
// New autodismiss feature!
<x-alert status="success" autodismiss="4">Disappears in 4 seconds.</x-alert>

customElements.define('x-alert', class extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    ...
  }

  static get observedAttributes() { return ['autodismiss'] }

  // Called once before connectedCallback
  attributeChangedCallback(name, oldVal, newVal) {
    switch (name) {
      case 'autodismiss':
        const seconds = newVal ? parseInt(newVal) * 1000 : 4000;
        setTimeout(() => this.dismiss(), seconds);
    }
  }

  dismiss() {
    this.dispatchEvent(new CustomEvent('dismiss'));
    this.remove();
  }
Enter fullscreen mode Exit fullscreen mode

That's the biggest differences with TAC. Classes still play a role, but they are strictly single-purpose utility classes. I use them in the same way people use Tailwind but also for customizing my components:

<h2 class="font-italic">Italicized Heading</h2>
<div class="flex align-content-center">Flexbox container with center-aligned items...</div>

<x-alert status="success" class="mar-b-lg width-100">Alert component customized with some extra margin on bottom and 100% width.</x-alert>
Enter fullscreen mode Exit fullscreen mode

Been doing this for about ~5 years at companies large and small and it's been awesome because everyone knows HTML tag+attributes API, but not everyone knows BEM semantics or other methodologies. And since this really is legit HTML it works with literally any tech stack.

I have an open-source project if you want examples: github.com/jfbrennan/m-

Thread Thread
 
jasper91 profile image
Jasperrr91

Interesting bits of code to browse through and it certainly has its charm. I would however never implement such code in companies I work at. It's far harder to read than simple components in . Making it harder to maintain since with the current job market there's mainly juniors and medior developers joining companies. The main key is to keep your code as simple and maintainable as possible.

Thread Thread
 
jfbrennan profile image
Jordan Brennan

It’s just vanilla HTML, CSS, and JavaScript, so I’m not sure what else would be more familiar and simple than that, but thanks for the comment.

Thread Thread
 
xcmk123 profile image
xcmk123

@jfbrennan your demo github.com/jfbrennan/m- is really awesome. Is there any way to use that on ReactJS project ?

Thread Thread
 
jfbrennan profile image
Jordan Brennan

Thanks and yes you can use it with React, keep in mind React is the only framework that still isn’t fully compatible with Custom Elements. Preact is. See custom-elements-everywhere.com/

Collapse
 
eballeste profile image
Enrique Ballesté Peralta • Edited on

Same (sorta), I will forever be in love with SASS and BE (BEM without the modifiers).

I satisfy all 3 of of the mentioned pro's of CSS in JS with: SASS+BE, custom HTML elements, and CSS variables.

I approach everything as a component and use custom HTML elements with a corresponding sass file with mixins only used to tap into our global design systems like our grids/buttons/fonts.

To me using BEM's modifier classes are equally as ugly (and annoying .block__element--modifier??? blegh!) as using utility classes and I absolutely hate referencing them in my JavaScript files. I restrict the use of modifiers for side-effects of user interactions which are modified via JS so I use custom HTML attributes as modifiers instead and do not worry about it bleeding into other components since I am taking advantage of SASS's locally scoped nature. I also keep SASS Elements one level deep in terms of nesting. My SASS components are usually flat and I only nest when doing something like the lobotomized owl.

For getting colors or dimensions from JS into the stylesheet, i inject custom css variables into the DOM element's style attribute and reference it in the css:

<CustomHTMLEl>
  <div class"CustomHTMLEl__container">
    ...content
  </div>
</CustomHTMLEl>
Enter fullscreen mode Exit fullscreen mode
  const $customEl = document.querySelector('CustomHTMLEl');
  const bgColor = '#32fa45';

  if (!$customEl) { return; }
  $customEl.style.setProperty('--bgColor', bgColor);


  const $hideBtns = document.querySelectorAll('HideCustomElBtn');

  if (!$hideBtns.length) { return; }
  $hideBtns.forEach(($btn) => {
    $btn.addEventListener('click', (e) => {
      $customEl.setAttribute('hidden', true);
    });
  });
Enter fullscreen mode Exit fullscreen mode

and in your SASS:

CustomHTMLEl {
  display: block;
  background: var(--bgColor);
  padding: 40px 0;

  @include media('>=md') {
    padding: 80px 0 120px;
  }

  &[hidden] {
    display: none;
  }

  .CustomHTMLEl {
    &__container {
      @include grid-container;
      grid-template-columns: 1fr;
    }

    &__content {
      display: flex;
      flex-direction: column;
      align-items: center;

      > * + * {
        margin-top: 16px;
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Works beautifully.

Collapse
 
airtonix profile image
Zenobius Jiricek

Another thing I would mention is that your nesting of media queries is also a thing i loathe.

the pattern I follow is :

thing/
  index.scss
  thing.scss
  thing-mediumup.scss
  thing-largeup.scss
  thing-xxxxxxxxxxxxlargeup.scss
Enter fullscreen mode Exit fullscreen mode

thing/index.scss

@import "./thing.scss";

@include media('>=md') { 
  @import "./thing.mediumup.scss";
}
@include media('>=lg') { 
  @import "./thing.largeup.scss";
}
@include media('>=xxxxxxxxxxxxlg') { 
  @import "./thing.xxxxxxxxxxxxlargeup.scss";
}
Enter fullscreen mode Exit fullscreen mode

This way, you're following a mobile first approach and then you don't end up with this :

CustomHTMLEl {
  display: block;
  background: var(--bgColor);
  padding: 40px 0;

  @include media('>=md') {
    padding: 80px 0 120px;
  }

  &[hidden] {
    display: none;
  }

  .CustomHTMLEl {
    &__container {
      @include grid-container;
      grid-template-columns: 1fr;
      @include media('<=md') {
        padding: 80px 0 120px;
      }
    }

    &__content {
      display: flex;
      flex-direction: column;
      align-items: center;

      > * + * {
        margin-top: 16px;
        @include media('<=sm') {
          padding: 80px 0 120px;
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

It's a pretty simple example, but I've seen worse.

Thread Thread
 
markohologram profile image
Marko A

Nesting media queries is one of the reasons I still love and use SASS. It allows me to immediately see all behavior for a single piece of styling. I don't have to go into 3-4 files (like in your case) in order to see how this component behaves on each resolution.

But to each their own, we all have our own preferences when it comes to some of this stuff. To me personally it's just easier to piece all of this stuff together when it's all in the same file, I can just immediately see "Oh this selector behaves like this on mobile, after 'md' and after 'lg' breakpoints".

Thread Thread
 
eballeste profile image
Enrique Ballesté Peralta • Edited on

same! multiple sass files for a single component? seems like a step backwards to me, harder to see the whole picture.

Thread Thread
 
microcipcip profile image
Salvatore Tedde

With this pattern you don't know how a component behaves on different breakpoints. If the component is complex is even more so, since when you change one element you have to check 4 files to make sure that it works as you intended. I highly discourage this pattern, nested media queries seems the natural way of styling a component IMHO.

Thread Thread
 
jasper91 profile image
Jasperrr91

Indeed, it's much easier to work with media queries that are simply called whenever they needed on a small bit of styling; than splitting entire stylesheets based on those same media queries.

Yes, you might up with a few more media queries but at least it's very easy for each developer to see what's going on. Having to look into separate files for the styling of a single component, as well as remembering what's happening for every single viewport, is a tremendous pain and unscalable nor maintainable.

Thread Thread
 
eballeste profile image
Enrique Ballesté Peralta • Edited on

also, they implied that we aren't doing mobile first using this approach which we 100% are and it's very easy to see that we are.

if you are a stickler for order then you would start from the bottom up and declare the padding at the mobile level and later reset for wider devices

    &__container {
      @include grid-container;
      grid-template-columns: 1fr;
      padding: 80px 0 120px;
      margin-top: 16px;

      @include media('>=md') {
        padding: 0;
        margin-top: 24px;
      }
    }
Enter fullscreen mode Exit fullscreen mode

but I also don't mind the convenience of using include-media to specifically target mobile if I don't want to use the bottom up approach. it isn't hard at all to reason about and I use it sparingly, only when I know it's going to be mostly mobile only styles.

    &__container {
      @include grid-container;
      grid-template-columns: 1fr;
      margin-top: 16px;

      // mobile only
      @include media('<md') {
        padding: 80px 0 120px;
      }

      @include media('>=md') {
         margin-top: 24px;
      }
    }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
airtonix profile image
Zenobius Jiricek • Edited on

I don't care about things being ugly.

My main goal is scaling in large teams and maintenance long term, so I care more about understanding dependencies and avoiding the cascade.

Looks like you've also dived deep into the problem 👍️

Thread Thread
 
eballeste profile image
Enrique Ballesté Peralta • Edited on

i guess it's the designer in me, but, to me, if it ain't beautiful, it ain't worth looking at.

Collapse
 
ajinkyax profile image
Ajinkya Borade

Good old BEM days with Knockout JS, not many React devs would know about BEM styling

Collapse
 
zohaib546 profile image
Zohaib Ashraf

BEM is really good i learnt from jonas sass course and it was really fun utilizing block, element and modifier classes

Collapse
 
allanbonadio profile image
Allan Bonadio

"Nested CSS rules ... are a maintenance nightmare."

Use things they way they're intended. If css rules are only intended to work in, say, tables, then nest them inside of tables. Or, just certain classes of tables. If they are to be general, then don't nest them.

table.french {
    .hilite { color: orange; }
}
table.italian {
    .hilite { color: green; }
}
.hilite { color: blue; }
Enter fullscreen mode Exit fullscreen mode

These three .hilite classes will not conflict with each other; the deeper ones will always override the shallow one at the bottom, but only inside .french or .italian tables. Hilite will be orange in french tables; green in italian tables, and blue everywhere else in your whole app. If you want .hilite to do something else outside of your zoot-suit panel, write it that way:

.zoot-suit {
    table.french {
        .hilite { color: orange; }
    }
    table.italian {
        .hilite { color: green; }
    }
    .hilite { color: blue; }
}
Enter fullscreen mode Exit fullscreen mode

Any HTML outside of the .zoot-suit panel will have to make their own style rules; they can use .hilite freely without any orange, green or blue bleeding in.

Collapse
 
markohologram profile image
Marko A

Only issue I have with this is that then you don't know if .hilite is something global, is it a utility class, is it something specific to that table only when you encounter it in HTML/JSX. If using .table__cell--hilite for example you know that it's a highlighted table cell and you can get all that data without having to look at the source code. And you know that it should be a table only thing here since it's kinda namespaced with table__

Also, your example uses same class name for global selector and then selector that is more specific to 2-3 scenarios which now makes that selector super situation specific and it doesn't necessarily convey its meaning just by reading it. In my opinion it also raises CSS selector specificity unnecessarily.

But to each their own, I have my preferences and you have yours. Lot of this stuff is of course also situational and cannot really be applied to 100% of projects/situations.

Collapse
 
latobibor profile image
András Tóth

Mixins... I FKN love them. They are excellent at naming certain CSS "hacks": things that only make sense together, things that must act together otherwise the effect you want won't happen. Now you could write comments around it, but named mixins equate well-named, descriptive function names from clean coding.

Collapse
 
airtonix profile image
Zenobius Jiricek

if they could be strongly typed I might agree with you, but all they end up doing is hiding complexity.

Thread Thread
 
latobibor profile image
András Tóth

Sorry, I don't get it. Do you have an example?

Collapse
 
leob profile image
leob

Always good to see some contrary opinions - not everything that "shines" is made of gold :)

Collapse
 
halfist profile image
Halfist

To me both HTML-in-JS and CSS-in-JS look ugly, because you can't separate presentation from logic, hence can't divide this work between different developers without permanent merge conflicts.

Collapse
 
wintercounter profile image
Victor Vincent • Edited on

I honestly disagree both with the article and some of the comments here. I'd like to address the first of the article. (Respect for the article, it does have valid points and concerns and it's well put together).

I have a lot of experience with a lot of solutions in a lot of different scenarios. A new solution to a current one can always feel better, because it excells in the painpoints the older solution had, those are the problems we're trying to address with the change, but that doesn't necessarly mean that the old solution wasn't performing better on other areas. For me CSS-in-JS (with optional static extraction and/or 0 runtime) is the perfect way, because it addresses many different issues I had through the years the most.

There are solutions that can static extract with 0 runtime and they are able to handle dynamic props using CSS variables, so at the end it's 0 runtime and only class names. I see you mentioned this at the end also, but there are some misconceptions here. The mentioned "problems" are not necessarly unique to 0 runtime libraries, there are solution which aren't suffering from those pain points.


The Bad

  1. CSS-in-JS adds runtime overhead: there are many compile time options that have 0 runtime using multiple different kinds of static extraction strategies.
  2. CSS-in-JS increases your bundle size: Just like the above.
  3. CSS-in-JS clutters the React DevTools. Static extracted styles will replace your styles with classNames and/or CSS variables. No clutter (not necessarily true to all solutions).

The Ugly

  1. Frequently inserting CSS rules forces the browser to do a lot of extra work: static extraction solves this problem.
  2. With CSS-in-JS, there's a lot more that can go wrong, especially when using SSR and/or component libraries: statically extracted, nothing can really go wrong, becuase there is nothing being done in JS.

Performance

No JS involved, I don't want to go into it more deeply than that.

Styles are still inserted when a component mounts for the first time... Not necessarily true. It's really based on the extraction strategy you choose. You can choose to extract everything in a single file. You will end up with the same "problem" using CSS Modules if you don't static extract into a single file btw.

Inline styles are known to cause suboptimal performance when applied to many elements... They can, but it's nothing you should worry about, especially that it's just CSS variables. The overhead of loading an external stylesheet and handling dynamic cases with code (conditions, className concat, etc) will actually perform worse by adding more overhead imo.


BEM

I'm incredibly against it. 1 HUGE benefit of CSS-in-JS (inline) is that you no longer need to think of naming and organizing your selectors/styles. Best conventions are the ones that are not needed ;) Naming things, especially using BEM is a huge overhead during development, it significantly interrupts your flow.

CSS Modules

  • Separate Stylesheets per module still need to be loaded dynamically if you want to bundle split. If you extract per component: extra request + extra style node; if you bundle: extra js size, runtime insert, extra style node.
  • Extract into 1 stylesheet: essentially same as a 0 runtime, static extracted CSS-in-JS with the overhead of maintaining styles separately with a separate system and/or language, with a separate config system (often duplicated to access values in js also) for no extra benefit/reason.
  • Naming stuff shouldn't be necessary. Less convention ftw!
  • "Separation of concerns" is what they say when putting CSS in a separate file. My personal take on this, they are right but apply it wrong. Both JSX and your styles are the same concern: view. Having deeply close together helps you understand and see immediately what your layout does, how it works, how it looks, just by looking there at the code. Separate them will cause slowdowns during development, you need to jump through several files just to check what is style.container actually is. Having them inline during development is a huge productivity boost for me.

CSS-in-JS with runtime

I'm not against runtimes tbh. I do have a fairly large social site with millions of contents and several multimedia types and complex user features. The complete client side codebase is 25MB in total bundle size (no gzip), yet my initial JS for a single page is 200k (no gzip), which is mostly React, it uses Styled-Components with CCSS and uses a custom, single file SSR pipeline (no Next or whatsoever) with an isomorphic codebase. I have my Lighthouse scores at 96-100 points. Could be faster w/o runtime? Yes. Would my users notice anything from it? Not really. So why would I choke myself by using a less productive/comfortable solution? :)

Extract on your own

It's not a too hard thing to write a Babel plugin that extracts the static parts of your css prop however you want. By that you save a lot of time spent on refactoring to a new solution on a large codebase.

Collapse
 
grsmto profile image
Adrien Denat

The performances issue was always a technological limitation but the DX of CSS-in-JS is the best to handle CSS in a codebase imo. Pre-compiled styles are solving this and it will keep on improving!
Now the reason this is better than Tailwind is that you do not need to learn anything new. As long as you know CSS you don't need a documentation. With Tailwind you're constantly juggling between the doc and your code (ok now we have auto-completion) and there is a learning curve. CSS-in-JS has a learning curve close to zero.

Collapse
 
kenakafrosty profile image
Ken aka Frosty

This is the most thought-through piece of writing on this entire page. Everything you wrote about SCREAMS "this developer is experienced AND practical and makes great points". Something felt slightly off about OP's post (like saying "inline styles are suboptimal for performance" but...citation needed??)

You should post an op-ed to this with the contents of this comment. It's a worthy article in itself. You have actual use-case-level information on how it doesn't even affect performance in any way a user would ever notice. That's a very valuable contribution to this topic and the entire community would be better for it.

Either way, thanks for the very well-constructed comment. It was very helpful in adding balance to this conversation.

Collapse
 
jfbrennan profile image
Jordan Brennan

The length React devs go to just to get component styles is beyond me. Riot, Vue, and others solved this like 6 or 7 years ago using…the <style> tag.

Collapse
 
spock123 profile image
Lars Rye Jeppesen

Angular as well, it's just much nicer to work with.

Don't even get me started with the awful react hooks.

Collapse
 
zainw profile image
Zain Wania

Overall fantastic analysis on css-in-js, learned alot for sure and appreciate the time you took to analyze everything.

I am struggling to understand how you reached your conclusion for picking bootstrap though. while bootstrap does have utility classes its main use case is telling your app how it ought to be.

Tailwind intends to provide a utility class framework with basic defaults. Tailwind does NOT enforce any styling on your components like bootstrap does with things like its btn class

If you are morphing bootstrap into something to fit your design system i believe you would be much better off creating a design system in Tailwind instead.

if you have more info on why you picked bootstrap besides that you were familiar with it, i would love to understand further because everythinbg described including the ovverides you are doing to bootstrap scream at me that you would be better off with tailwind

Collapse
 
srmagura profile image
Sam Magura

We're not using all of Bootstrap, just their utility classes (which we have customized quite a bit). I'm not very familiar with Tailwind but I think they may have taken utility classes a bit too far.

Collapse
 
nidemos profile image
niddy - the animated atheist

Tailwind will now tree-shake its styles down on build, removing all unused styles.

Thread Thread
 
jonaskuske profile image
Jonas

Afaik they don't tree-shake anymore. The default for quite some time now is the JIT runtime, which only creates the CSS rules that are needed in the first place :)

Collapse
 
spock123 profile image
Lars Rye Jeppesen

You will absolutely love Tailwind, trust me bro

Thread Thread
 
humoyun profile image
Humoyun Ahmad

Bro, I personally used Tailwind and I can it is really good solution but... it is really good solution for only a specific parts of styling, namely, for utility styles (mostly common & repetitive styles like positioning, padding, margins, flex, ...). But applying Tailwind for everything is a mess and vastly clutters codebase. Besides, it has steep learning curve (it may not be felt while you are working solo but in a team with noticeable numbers you can really see this). It has its own place in DX but it is definitely not a silver bullet.

Thread Thread
 
spock123 profile image
Lars Rye Jeppesen • Edited on

Wether it clutters the codebase or not is up to you.
There is nothing preventing you from making your normal classes and use the @apply directive.

.myClass {
@apply rounded-full mx-auto text-primary-500 my-8
}

So in that regard there is no difference.

Thread Thread
 
humoyun profile image
Humoyun Ahmad • Edited on

Yes. And try to manage hundreds or thousands of this kind of meta classes across a huge codebase. You are adding yet another unnecessary abstraction layer on top of already provided abstraction... Don't think in terms of just you coding by yourself and setting conventions on how to use it, maybe it works to some degree, but not in a team .

Collapse
 
tomrav profile image
Tom Raviv

We've reached a lot of similar conclusions here at Wix.com too. Our first HTML editor (and following versions) used an in-house CSS-in-JS solution, but eventually the performance costs just got too high.

I'm the team leader working on Stylable, our solution to this problem space. Stylable is a CSS superset that adds a type-system, automates the BEM scoping part, and provides templating tools to improve developer experience and reduce code duplications.

We've been battle testing it internally at Wix for several years now (used in over 50+ million sites), and are now beginning to reach out to new users and to build our community. We'd love to talk if this sounds interesting to you.

Collapse
 
srmagura profile image
Sam Magura

Hey @tomrav, Stylable looks cool. It would be interesting to hear what benefits it provides over something like Sass Modules. E.g. what problems does Stylable solve that are harder to address with existing tools?

Collapse
 
tomrav profile image
Tom Raviv

Glad you liked what you saw, I'll elaborate a bit on our approach.

Sass and CSS modules are both well proven and battle tested solutions that we absolutely love, but have their downsides too. I would say the most notable differences between Sass modules and Stylable are the following:

  • Sass is oriented for CSS and document based projects in its core. CSS modules tries to bend Sass' approach to be more component oriented. Stylable was built with components being treated as first class citizens, allowing stylesheets or their parts to be utilized by our module system in various ways.

  • The namespacing that CSS modules provides is there to prevent collisions, and not improve readability or debugging experience (unlike BEM, which in my opinion does both). While Stylable combines both approaches, opting to use a configurable automatic BEM-like namespacing for its output.

  • At Wix a single component can have many drastically different use cases - to provide flexibility for such components, Stylable encourages treating your component styling as just another part of its API. Each stylesheet exposes its inner parts allowing to customize and theme any part of the application easily from the outside.

  • Stylable adds a type-system to CSS (currently, only at the selector level, with more of the language to come). This allows defining inheritance and relationships across stylesheets, allowing our custom language service a much better understanding of the code, resulting in advanced diagnostics, auto-completions, code navigation and other language features.

  • Lastly Stylable namespaces a lot more than just class names, also namespacing keyframes, custom properties, layers and (very soon) container queries to keep you safe from leaks or collisions.

I'll be happy to answer any questions here, or chat over zoom sometime if you'd like to hear more.

Thread Thread
 
srmagura profile image
Sam Magura

@tomrav That sounds pretty awesome! I agree the BEM-style class names would help a lot with "debuggability" over the class names you get from CSS Modules.

It sounds like you have some pretty complex components at Wix. Or rather, you have components that need to be fully themable and work in many different contexts. We don't really have this problem at Spot so I am not sure if Stylable would be as necessary for us.

The additional namespacing features sound nice too. That is one thing that could make Sass hard to scale (though I'm not necessarily up to date on the @use directive in Sass, I still use @import.)

Feel free to email me if you'd like to talk more. srmagura(AT)gmail.com :)

Thread Thread
 
tomrav profile image
Tom Raviv

In Wix we definitely have some complex cases, but (as biased as I am), I feel that Stylable also offers a lot of value to simpler projects by providing an improved developer experience with type safety.

Collapse
 
ivanjeremic profile image
Ivan Jeremic

Charka-UI sweating right now reading this article😂

Collapse
 
primos63 profile image
Primo Sabatini

I don't think so. Full disclosure I was a contributor on Chakra-UI.

Sage has been aware of the issues with Chakra and it's in their doc. He's been working on ZagJS which uses state machines to provide unstyled components. I assume that one could use CSS, Tailwind or CSS-in-JS to style those components. This is a YouTube video of Sage talking to Lee Robinson about The Future of Chakra/ZagJS.

While I agree with the author about CSS-in-JS, being "the 2nd most active maintainer of Emotion", he's been a contributor to the problem and previously thought of as a contributor to THE solution. We've all been there. If you haven't yet, just wait.

Shouldn't React with their vdom be sweating given projects like Svelte, Solid and Qwik? If you laugh at that remember that once JQuery and Angular JS ruled the world. They were each great for their time, like Emotion, but eventually every king falls.

Collapse
 
mariomui_53 profile image
Yi Kan Mario Mui

What exactly is Svelte and Solid except the idea that surgical dom mutations are better than the complexity of a reconciliation engine. Now that we have modern optimized browsers, the svelte/solid model makes sense. A really big app with lots of dashboards and data feeds...do you really want that to be in svelte? Hrm.

There's also module federation where you sortah want css in js or you'd have to manually include a copy of extracted css into every federated module.
And you have to worry about tree shaking when its a .css file.
not that that's a big deal but it's an additional worry.

You could make the similar argument of using emotion to do dynamic css, and then using tailwind utility classes for the majority of your use case. Then you could have your cake and eat it too.

Collapse
 
srmagura profile image
Sam Magura

Hahahahah

Collapse
 
frontendtony profile image
Anthony Oyathelemhi

You should've just went with Tailwind tbh

Bootstrap was just convenient because you were already familiar with the classes but there's so much you could've gained by using Tailwind, like JIT compilation (on demand one-time utility classes, variants for element state and media queries, ability to change things using a configuration file, etc)

Collapse
 
spock123 profile image
Lars Rye Jeppesen

And Tailwind is not opiniated, unlike Bootstrap.

110pct agree with all your points

Collapse
 
jakelane profile image
Jake Lane

Hey lead on the Compiled CSS-in-JS project here 👋. Just thought I'd clear up some aspects of the Compiled library and our future roadmap.

For the components that are included at runtime, those are intended for use in development only. For production use, we're going to recommend people use stylesheet extraction which moves all the CSS into an atomic stylesheet. That'll mean that all the runtime react components will be stripped out. We haven't fully made this clear in documentation yet as we're still ironing out some issues with dev experience.

The dynamic prop support is intended as a migration feature to allow people to adopt compiled without fully re-writing their code. We required this as part of our migration journey at Atlassian. It does end up with non-optimal performance this way so we're we'll be building more tooling around writing fully static CSS.

At Atlassian, we did a deep dive into whether to use CSS modules or continue with CSS-in-JS. We did end up selecting Compiled as our best path forward for a few reasons, but to simplify:

  • Devs are familiar with CSS-in-JS and we can migrate without too much difficulty to a build time CSS-in-JS library. It didn't seem like a good use of time to re-write all our CSS.
  • We find CSS-in-JS works really well for our design system and the amount of CSS we have at Atlassian. We'd have a lot of new challenges around ensuring code quality, encapsulation, and reusability without it.
  • We can end up with the ~same performance characteristics of CSS modules with Compiled on the CSS prop - possibly better with tooling to make better use of atomic CSS.
Collapse
 
brense profile image
Rense Bakker • Edited on

Why people keep hyping tailwind is beyond me. Nothing about compiling a list of utility classes before you run your code, sounds at all useful to me... It literally prevents you from doing the easiest dynamic stuff because of the limitations of your utility classes and the only way around that is to do custom DOM manipulation to change css variables at runtime. Why do people still think this is a good idea? Why not use the benefits of css in js and have it compile to plain css files when building? Seems like a much more sensible and maintainable solution to me. Also, having all those utility classes that you don't use, creates far more overhead... CSS files have to be downloaded by the browser too... You literally have code in production that never gets used... I thought we moved past that :(

Collapse
 
iohansson profile image
iohansson

Sorry, but your argument about having unused utility classes is incorrect. Tailwindcss for a long time now compiles only the classes that are actually being used in your code.
Would be great to fact-check your arguments before you post since it might mislead those not familiar with the topic.

Collapse
 
brense profile image
Rense Bakker

Ok, what about the other argument?

Thread Thread
 
iohansson profile image
iohansson

I've been using tailwind with Vue mainly, for dynamic stuff you would just conditionally apply different utility classes, no need to use CSS vars. Can't complain about the DX.
The only negative side I can see here is that in order to reuse your components in another project that project would need Tailwindcss set up as well, so they're not as portable. But that might also be true for css-in-js solutions, right?
In this sense web components would be the best probably, I might be wrong though.

Thread Thread
 
brense profile image
Rense Bakker

You cant conditionally apply dynamic values. You also cannot access defined constants in tailwind with JavaScript, so if you need to apply custom logic based on theme breakpoints you have to define and manage them in two places.

Collapse
 
redbar0n profile image
Magne • Edited on

the only way around that is to do custom DOM manipulation to change css variables at runtime

I also thought so, but you can actually avoid custom DOM manipulation through changing a single theme class via your declarative rendering library (React):

youtube.com/watch?v=TavBrPEqkbY

When you do that, the top level component with that theme class will re-render with a new theme class (but none of it's children will need to re-render since their props didn't change so the React VDOM diff will exclude them). Then, due to the theme class being a CSS variable, all the local styles scattered about the app, that reference the CSS variable, will update with the new theme during the CSS cascade. That way, you avoid the rendering de-optimization that you'd otherwise have if you changed all the local styles through re-rendering the entire app with JS.

Collapse
 
dinsmoredesign profile image
Derek D

Have you ever looked into Vue's style system? I'm not sure how they do it, but you simply add a "scoped" attribute to a style tag and it automatically scopes the CSS to the components. Their CSS is also compiled at build time into its own file without the need to render separate components. This is the one thing that kept me from switching to React for years. Their styling just works and it works really well.

I haven't messed with Vue 3's implementation, but they also allow you to use JS variables, which are rendered as inline styles: vuejs.org/api/sfc-css-features.htm...

Collapse
 
carlldreyer profile image
Carl Lidström Dreyer

Vue adds a hashed attribute to the dom element, for example:
data-v-ikd87k9.

Then your CSS is transformed using PostCSS to target CSS-selectors with your attribute, for example:
.my-class[data-v-ikd87k9] {}

Collapse
 
clay profile image
Clay Ferguson

I consider it a train-wreck when I can't view my DOM and see all the class names. I don't like my class names being mangled like that hash thing you mentioned. I'm fine with SCSS and not having styles scoped to components, because lots of my styles are cross-component anyway.

Thread Thread
 
carlldreyer profile image
Carl Lidström Dreyer

I agree, personally, I consider a readable DOM to be important, which is why I try to avoid Tailwind :)
However, I've never experienced Vue's hashing to have impacted this negatively.

Thread Thread
 
spock123 profile image
Lars Rye Jeppesen

What about Tailwind is not readable? If anything it's more readable.

And nothing prevents you from making custom classes composed of Tailwind helper classes.

Thread Thread
 
mattaningram profile image
Mattan Ingram • Edited on

Tailwind is very difficult to read when you get into more complex cases, especially when working on teams where not everyone knows Tailwind utilities. Having a class that actually describes what the purpose of the element is in addition to styling it is far more readable, takes up far less space, and can still allow for the use of utility classes where they are appropriate for example adding layout and margins.

Vue does not mess with the class names you add to elements, it just appends a data-hash attribute.

Collapse
 
spock123 profile image
Lars Rye Jeppesen

Angular does this by default.
Very very nice

Collapse
 
teetotum profile image
Martin

Thank you for sharing your insights. I agree (judging from my current view point of past and present projects and experiences) that CSS-modules paired with SASS/SCSS are the cleanest and sanest approach to component styles.
But I would say it also checks the third point of your three-point list of good things (locally-scoped styles, co-location, JavaScript variables in styles):
You can share values between styles and js code, in both directions, during build time and during runtime. Each scenario requires its own mechanism (I want to cover this in a blog post but don't know when I will find the time), here is the gist of it:

  • from css to js, build time: use :export
  • from css to js, runtime: use getComputedStyle()
  • from js to css, build time: use sass-loader additionalData option
  • from js to css, runtime: use element.style.setProperty('--some-prop', value)

In my webpack config I share the colors (in a *.ts file) with my styles by generating variables like $primary-main:

import { colors } from './colors';
import { jsToScss } from './js-to-scss';

{
  loader: 'sass-loader',
  options: {
    additionalData: jsToScss(colors),
  },
},
Enter fullscreen mode Exit fullscreen mode
Collapse
 
renancferro profile image
Renan Ferro

Nice article man, this helped me how to improve the implementation in Angular too!

Collapse
 
allanbonadio profile image
Allan Bonadio

The good:
1 Locally-scoped styles:
Solution: Use hierarchical cascading styles, the way CSS was designed to be used.

.WaveView { position: relative; }
.WaveView .row { position: absolute; }
Enter fullscreen mode Exit fullscreen mode

.row will never affect anything that isn't inside of a .WaveView ; then you can reuse .row somewhere else, which is similarly contained inside another element. Even something that has nothing to do with position. Each element creates a new namespace for classes that you can use.

I highly recommend Scss, Sass, Less, Stylus or similar packages that allow you to write like this:

.WaveView {
    position: relative;
    .GLView {
        position: absolute;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, you know Exactly what affects what; you've got local scopes staring you in the face. Added bonus: you can see which div is which in the debugger, without guessing what the hashed names are. No 'modules' files wandering around are needed. No funny junk is being inserted into your HTML that you don't know about.

For BIG apps, you can add levels to the hierarchy. Each level adds another unique namespace. You will never run out.

.TrainPage {
    .NorthPanel {
        .WaveView {...
            .GLView {...
Enter fullscreen mode Exit fullscreen mode

If some bonehead put in a global .row class, take it out immediately, otherwise everybody else who uses .row will base their CSS on it, and it'll be harder to remove later. Or, Better, get the programmer who put it in to remove it and fix all resulting glitches. See also DELETE FROM mytable in SQL.

2 Colocation:
Put your .css (or .scss or whatever) files right next to your .js files. You don't need to separate them in different directories; in fact, don't. That's what the suffix is for, so there's no conflict! Neither will lose the other. Also put the .spec.js file right next to those two, so you don't forget about it.

3 You can use JavaScript variables in styles
You can ALREADY do this in react:

<p style={{
    color: colors.primary,
    fontSize,
    border: `1px solid ${colors.border}`,
}}>
Enter fullscreen mode Exit fullscreen mode

LEARN THE TOOLS YOU ALREADY KNOW, the ones already taking up code space in your app's footprint, and brain space in your head.

The Neutral: 1. It's the hot new technology.
NEVER glom on yet another package for this reason! Only incorporate a new package because you really need it. And, because it adds significant new capabilities; if it's simple, just write it yourself so you have more control over it. I've written too much code to get around misfeatures in packages that I could have just written myself the way I want it.

I see packages all over that do exactly what you can do if you just use the right control panel options, or the right HTML or React features. Read the docs for HTML, CSS, JS, React, etc; often it's already built in. (Otherwise, you'll have to read the docs for the package you're adding on anyway, right?) HTML5, CSS3 and ES10 have awesome new features; learn them now before you write more code, painfully, without them.

Every time you add on somebody else's software, you run the risk that it'll introduce another bug somewhere, and you won't notice it until you've installed a dozen more packages, so you won't have a clue where the problem is. Better to let the OS people or the HTML people or the React people or whomever incorporate the feature - they know the HTML code & React code best, and they'll make sure it still works next major release.

Collapse
 
baptistefkt profile image
Baptiste Firket

So, following your logic, nobody should use JS framework either and use vanilla JavaScript.

I think the assumption that those CSS-in-JS library are so popular because "it's hot and new technology" is wrong. They are popular because they make developer life easier. Of course there are ways to achieve the same without them. But it's easier and faster. I don't want to use BEM, I don't want write and concatenate classNames, I don't want to use object style css like in your 3rd example. I do want to be able to access my component props in my css out of the box, etc.

The problem is that most of developer (like me) are not aware of the performance cost (appart from bundle size).. So big thanks to the author of this article to highlight it, and I personally think that a solution like Linaria is very promising.

Collapse
 
allanbonadio profile image
Allan Bonadio

"So, following your logic, nobody should use JS framework either and use vanilla JavaScript."

no, that's not what I said.

Let's say you need to recognize and parse postal codes, US, Mexican and Canadian. So you find a package that does ALL postal codes in the world. It's OK at first, but you run into complications. User types in a postal code with a mistake - and suddenly the package decides that it's a Turkish postal code, so it doesn't flag an error. You have to pass weird options to get it to stop, using Intl.* stuff. And, you had to futz with it for a while to figure out which options. Meanwhile, it doesn't do the "+4" part of US zip codes. And, there's other problems - there's 200 countries in the world!

So the issue is, the package has features that you don't need, and they get in the way. In that case, check and see if what you want is easy to do by hand. In this case, you can write a few regexes, and a few IF statements, and it works exactly the way you want. That's the way to do it.

Now, maybe you could find a package that parses US, Mexican and Canadian postal codes. Looks good. But, you still have to read the docs - somebody else invented it, not you. And, upgrade to newer versions. And, run the risk that they have bugs, and you don't know how to fix them. Then you find a fix, but you can't change the package itself, so you have to add your own code to work around the bug. Then, they come up with a new version, and you wonder if they fixed that bug, or if the new version doesn't work with your fix, so...

This might sound contrived to you, but I've had each one of the above problems happen to me before. Once I made a script that would go in and hack the package; it would run every time I installed it.

One time, I had this really, really weird bug. A jQuery had a jQ function 'format'. Worked for a while, but then I tracked down a bug to the fact that the 'format' function was gone. Just not there. I checked out the package, downloaded again, 'format' is there, everything was fine till the software ran, and the format function was gone. I finally figured out that this other jQ package also had had a function named 'format', but not in the version I was using. So, the new version got rid of it. Literally. It deleted anything named 'format, even if it wasn't their format function. I wasted so much time on that.

That's what I'm talking about when I say, each package you use is a small liability, and if it just does something simple, it might be better to write it yourself.

Collapse
 
sorenhoyer profile image
Søren Høyer • Edited on

As others have mentioned you forgot to mention PostCSS.
Also, you only do have 24 commits but make it sound like much more than it is...

Edit: Airbnb recently moved from SASS to Linaria and wrote a great article about it. I recommend people reading this one also have a look at medium.com/airbnb-engineering/airb... - judging from that, Linaria might just be the best pick.

Collapse
 
leob profile image
leob

Fantastic ... this article might have saved me from unknowingly going down a road that I really shouldn't go down, at some point ... saving this article in a place where I'll be able to find it again.

Want to Create an Account?
Now it's your turn!
 
🗒 Share a tutorial
🤔 Reflect on your coding journey
❓ Ask a question

Create an account to join hundreds of thousands of DEV members on their journey.