Skip to main content

Anda (mungkin) tidak membutuhkan CSS-in-JS

00:07:44:53

Ketika saya pertama kali mencoba pustaka CSS-in-JS seperti Styled Components dan Emotion, hal yang dirasa benar adalah meneruskan nilai atau status langsung ke gaya untuk sebuah komponen. Itu benar-benar menutup loop dengan konsep React di mana UI adalah fungsi negara. Meskipun ini merupakan kemajuan yang pasti dibandingkan cara tradisional menata gaya React dengan kelas dan CSS yang telah diproses sebelumnya, hal ini masih mempunyai masalah.

Untuk menyoroti beberapa contoh, saya akan menguraikan beberapa contoh umum menggunakan dua tipe utama gaya dinamis yang akan Anda temui dengan komponen React:

  1. Values: seperti warna, penundaan, atau posisi. Apa pun yang mewakili nilai tunggal untuk properti CSS.
  2. States: seperti varian tombol utama, atau status pemuatan, masing-masing memiliki serangkaian gaya terkaitnya sendiri.

Dimana kita saat ini

Sebelum kita mulai, sebagai perbandingan, saya akan menggunakan SCSS (dengan syntax BEM) dan Komponen Bergaya dalam contoh saya tentang bagaimana gaya biasanya didekati di React. Saya tidak akan membahas perpustakaan CSS-in-JS yang berhubungan dengan penulisan CSS sebagai objek JavaScript. Saya pikir sudah ada solusi bagus di luar sana (saya akan merekomendasikan Vanilla Extract) jika Anda lebih suka melakukan pengecekan tipe dan hidup lebih sepenuhnya di sisi JavaScript. Solusi saya lebih ditujukan bagi kita yang suka menulis CSS sebagai CSS, namun ingin merespons reaktivitas dan status komponen dengan cara yang lebih baik.

Jika Anda sudah familiar dengan masalahnya, kita bisa lanjut ke solusinya.

Values

Menggunakan vanilla CSS, atau pre-processed CSS melalui LESS atau SCSS, cara tradisional untuk meneruskan a value ke gaya Anda adalah dengan hanya menggunakan gaya sebaris. Jadi jika kita memiliki komponen tombol yang memungkinkan warna, maka akan terlihat seperti ini:

jsx
function Button({ color, children }) {
  return (
    <button className="button" style={{ backgroundColor: color }}>
      {children}
    </button>
  );
}

Masalah dengan pendekatan ini adalah pendekatan ini membawa serta semua masalah gaya inline. Sekarang memiliki spesifisitas yang lebih tinggi sehingga lebih sulit untuk diganti, dan gayanya tidak ditempatkan bersama dengan gaya tombol lainnya.

CSS-in-JS (dalam kasus Komponen Styled Components atau Emotion) memecahkan masalah ini dengan mengizinkan nilai dinamis seperti ini secara langsung sebagai props

jsx
// We can pass the `color` value into the styled component as a prop
function Button({ color, children }) {
  return <StyledButton color={color}>{children}</StyledButton>;
}

// The syntax is a little funky, but now in the styled component's styles
// we can use its props as a function
const StyledButton = styled.button`
  border: 0;
  border-radius: 4px;
  padding: 8px 12px;
  font-size: 14px;
  color: dimgrey;
  background-color: ${props => props.color};
`;

States

Traditionally, we'd use css classes and concatenate strings. This always felt messy and clunky, but it works nicely on the css side, particularly if you're using a naming convention like BEM along with a pre-processors. Say we have small, medium, and large button sizes, and a primary variant, it might look something like this:

jsx
function Button({ color, size, primary, children }) {
  return (
    <button
      className={['button', `button--${size}`, primary ? 'button--primary' : null]
        .filter(Boolean)
        .join(' ')}
      style={{ backgroundColor: color }}
    >
      {children}
    </button>
  );
}
scss
.button {
  border: 0;
  border-radius: 4px;
  padding: 8px 12px;
  font-size: 14px;
  color: dimgrey;
  background-color: whitesmoke;

  &--primary {
    background-color: $primary-color;
  }

  &--small {
    height: 30px;
  }

  &--medium {
    height: 40px;
  }

  &--large {
    height: 60px;
  }
}

The SCSS is looking nice and clean. I've always liked the pattern of using nesting to concatenate elements and modifiers in SCSS using the BEM syntax.

Our JSX, however, isn't faring so well. That string concatenation on the className in the is a mess. The size property isn't too bad, because we're appending the value directly onto the class. The primary variant though... yuck. Not to mention the wacky filter(Boolean) in there to prevent a double space in the class list for non-primary buttons. There are better ways of handling this, for example the classnames package on NPM. But they only make the problem marginally more bearable.

Unlike dynamic values, Styled Components is still a bit cumbersome in dealing with states

jsx
function Button({ color, size, primary, children }) {
  return (
    <StyledButton color={color}>{children}</StyledButton>
  }
);

const StyledButton = styled.button`
  border: 0;
  border-radius: 4px;
  padding: 8px 12px;
  font-size: 14px;
  color: dimgrey;
  background-color: whitesmoke;

  ${props => props.primary && css`
    background-color: $primary-color;
  `}

  ${props => props.size === 'small' && css`
    height: 30px;
  `}

  ${props => props.size === 'medium' && css`
    height: 40px;
  `}

  ${props => props.size === 'large' && css`
    height: 60px;
  `}
`;

It's not terrible, but the repeated functions to grab props gets repetitive and makes reading styles quite noisy. It can also get way worse depending on the type of state. If you have separate but mutually exclusive states sometimes it calls for a ternary expression that can end up looking even more convoluted and difficult to parse.

jsx
const StyledButton = styled.button`
  border: 0;
  border-radius: 4px;
  padding: 8px 12px;
  font-size: 14px;
  color: dimgrey;

  ${props =>
    props.primary
      ? css`
          height: 60px;
          background-color: darkslateblue;
        `
      : css`
          height: 40px;
          background-color: whitesmoke;
        `}
`;

If you're using Prettier for code formatting like I do, you'll end up with a monstrosity like you see above. Monstrosity is a strong way of putting it, but I find the indentation and formatting really difficult to read.


There's a better way: vanilla CSS

The solution was with us all along: CSS custom properties (AKA CSS variables). Well, not really. When the methods I've covered above were established, CSS custom properties weren't that well supported by browsers. Support these days is pretty much green across the board (unless you still need to support ie11).

After making the journey through using SCSS to Styled Components, I've come full circle back to vanilla CSS. I feel like there's an emerging trend of sticking more to platform standards with frameworks like Remix and Deno adhering closer to web standards instead of doing their own thing. I think this will happen with CSS as well, we won't need to reach for pre-processors and CSS-in-JS libraries as much because the native features are becoming better than what they have to offer.

That being said, here's how I've approached styling React components with vanilla CSS. Well, mostly vanilla CSS. I'm using postcss to get support some up and coming features like native nesting and custom media queries. The beauty of postcss is that as browsers support new features, the tooling slowly melts away.

Values

A really neat trick I've found for passing values into css is using custom properties. It's pretty simple, we can just drop variables into the style property and it works.

jsx
function Button({ color, children }) {
  return (
    <button className="button" style={{ '--color': color }}>
      {children}
    </button>
  );
}
css
.button {
  border: 0;
  border-radius: 4px;
  padding: 8px 12px;
  font-size: 14px;
  color: dimgrey;
  background-color: var(--color);
}

Now you might be thinking "isn't this just inline styles with extra steps?", and while we are using inline styles to apply the variable, it doesn't come with the same downsides. For one, there's no specificity issue because we're declaring the property under the .button selector in the css file. Secondly, all our styles are co-located, it's just the value of the custom property that's being passed down.

This also makes it really convenient when working with properties like transforms or clip-paths where you only need to dynamically control one piece of the value

jsx
// All we need to pass is the value needed by the transform, rather than
// polluting our jsx with the full transform in the inline style
function Button({ offset, children }) {
  return (
    <button className="button" style={{ '--offset': `${offset}px` }}>
      {children}
    </button>
  );
}
css
.button {
  border: 0;
  padding: 8px 12px;
  font-size: 14px;
  color: dimgrey;
  background-color: whitesmoke;
  transform: translate3d(0, var(--offset), 0);
}

There's way more you can do with CSS custom properties, like setting defaults and allowing overrides from the cascade for any components that compose one another to hook into, like a "CSS API". This article from Lea Verou does a great job at explaining this technique.

States

The best way I've found to deal with component states and variants with vanilla CSS is using data attributes. What I like about this is that it pairs nicely with the upcoming native CSS nesting syntax. The old technique of targeting BEM modifiers with &--modifier doesn't work like it does in pre-processors. But with data attributes, we get similar ergonomics

jsx
function Button({ color, size, primary, children }) {
  return (
    <button className="button" data-size={size} data-primary={primary}>
      {children}
    </button>
  );
}
css
.button {
  border: 0;
  border-radius: 4px;
  padding: 8px 12px;
  font-size: 14px;
  color: dimgrey;
  background-color: whitesmoke;

  &[data-primary='true'] {
    background-color: var(--colorPrimary);
  }

  &[data-size='small'] {
    height: 30px;
  }

  &[data-size='medium'] {
    height: 40px;
  }

  &[data-size='large'] {
    height: 60px;
  }
}

Have a play with the example button component here:

This looks similar to how modifiers are written using BEM syntax. It's also much more straightforward and easy to read than the Styled Components function syntax. The one downside is that we do gain a level of specificity that we don't with BEM modifiers using the &--modifier pattern, but I think that's an acceptable tradeoff.

It may seem kinda weird at first to use data attributes for styling, but it gets around the problem of messy string concatenation using classes. It also mirrors how we can target accessibility attributes for interaction-based styling, for example:

css
.button {
  &[aria-pressed='true'] {
    background-color: gainsboro;
  }

  &[disabled] {
    opacity: 0.4;
  }
}

I like this approach because it helps structure styling, we can see that any class is styling the base element, andy any attribute is styling a state. As for avoiding style clashes, there are better options now that automate the process like CSS Modules which is included out of the box in most React frameworks like Next.js and Create React App.

Of course, these techniques don't require you to only use vanilla CSS, you can just as easily combine them with CSS-in-JS or a pre-processor. However with new features like nesting and relative colors I think it's becoming less necessary to reach for these tools.

The entirety of this website is styled using these techniques, so if you want to see an example of how this applies to some real components, take a gander at the source code.