The Ultimate Guide to CSS in React

Curious about CSS in React? or React-Powered Solutions? As a front-end developer, you know the value of CSS in web development, but how does it work with React? Join me as we will be covering everything from the fundamentals to more advanced techniques.

Vanilla CSS

Like most, I began my web development journey with vanilla CSS - a simple approach that only requires a single HTML and CSS file.

index.html
<h1 class="title">Hello World!</h1>
index.css
.title {
    font-size: 32px;
}

As I created bigger applications, I gradually realized some of its drawbacks.

  • Lack of reusability: It's easy to create overly complicated stylesheets that are difficult to reuse.
  • Global Namespace. Since CSS has a global namespace, it can accidentally target unintended elements.
  • Co-location. Modularity is difficult to achieve, making it challenging to safely remove unused code.

I was later introduced to CSS pre-processors, which are tools designed to add new functionality to CSS and compile it back to vanilla CSS.

CSS pre-processors

I've likely used every CSS preprocessors out there in building design systems, including Sass/Scss, Sass-on-demand, Less, Stylus, PostCSS, and Stylecow. Each has slightly different syntax. PostCSS and Stylecow provide modern features and improved syntax. As a developer, it's important to choose the one that suits your project's needs and preferences. Now, let's dive into Sass.

Sass

Sass, a widely-used preprocessor, offers a way to write more reusable and maintainable CSS. It comes with several features, such as variables, mixins, and modules, that are very popular among developers.

  • Variables. This feature enables me to define global values just once and use them throughout my stylesheets.
  • Mixins. With this feature, I can easily reuse specific snippets of CSS without having to write them again and again.
  • Modules. This feature helps me modularize my CSS files, making it easier to manage and delete code safely.
_base.scss
$font-stack: Helvetica, sans-serif;
$primary-color: #333;
body {
    font: 100% $font-stack;
    color: $primary-color;
}
_header.scss
@use 'base';
.header {
    font-size: 32px;
}

As I used Sass to write more modular and reusable CSS, I discovered some drawbacks over time:

  • Naming. As the CSS codebase grows, it can be challenging to maintain naming conventions, and class names can quickly become unwieldy.
  • Browser support. Some features of CSS are not supported across all browsers and require the use of vendor prefixes like -moz- (Firefox), -webkit- (Safari), and -ms- (Internet Explorer and Edge) for cross-browser compatibility.

To address naming challenges, I adopted the Block Element Modifier (BEM) methodology. BEM helps create reusable components and organizes CSS in a modular way, keeping specificity low.

<button class="button">Cancel</button>
<button class="button button--primary">Submit</button>
.button {
    color: black;
    background-color: gray;
}
.button--primary {
    color: white;
    background-color: blue;
}

While naming conventions helped with CSS naming, it didn't fully address the root cause. To handle vendor prefixes, it's possible to set up your Sass toolchain with a tool like autoprefixer. However, when I started using component-based frameworks like React, I delved into exploring CSS-in-JS.

CSS-in-JS

I delved into CSS-in-JS while building a component library, and it seemed to solve all my problems initially.

  • Specificity was resolved through auto-generated class names.
  • Colocation was addressed by placing CSS directly with the component, making it easy to remove code.
  • Browser support was tackled by having autoprefixer integrated.
  • Variables were resolved by establishing a global theme.

Despite the numerous advantages, I still encountered difficulties with naming.

As the component library grew, I realized the need for a more structured approach to scaling CSS-in-JS and enforcing a design system. That's when I came across Theme UI and styled-system, which not only helped me with scaling but also solved the issue of naming.

When I attempted to scale styled-components alone in a large application, I had to resort to creative naming approaches, such as using words like container, wrapper, or layout, which quickly became unmanageable. For example:

const HomeContainerWrapper = styled.div`
   padding: 8px;
   font-weight: bold;
   color: white;
   background: blue;
`;
<HomeContainerWrapper>
  Hello
</HomeContainerWrapper>
// Forget about naming
<Box
  padding={3}
  fontWeight='bold'
  color='white'
  bg='blue'
>
  Hello
</Box>

Now, I know what you're thinking. Inline styles 🤮. At first, I was hesitant too. But eventually, it won me over - and that's where I am today.

Present State

Lately, I've been exploring CSS Modules and Tailwind CSS to style my React apps, leading me to take a closer look at the ecosystem.

Here's a breakdown of the pros and cons of different approaches to styling your React application. I've aimed to keep this comparison as impartial as possible. Your preferred option depends on:

  • Your familiarity (stick with what you're comfortable).
  • Your team.
  • The size of your application.
  • Your project requirements.

Pros and Cons

Vanilla CSS

You might use vanilla CSS if...
No additional toolchain required, works with every browser and language.
You might not use vanilla CSS if...
Not suitable for large-scale web applications.

CSS Modules

CSS Modules can be a suitable choice for smaller teams that do not need to share components across applications.

Next.js has built-in support for CSS Modules using the .module.css extension, removing the need for Webpack configuring. Additionally, it comes with autoprefixer pre-installed, making CSS Modules a compelling option compared to CSS-in-JS in certain scenarios.

You might use CSS modules if...
You need component-specific scoped class names.
You want to reuse CSS variables.
You require your app to work without JavaScript.
You want the lowest barrier to entry.
You want to use any language, not just JavaScript.
You might not use CSS modules if...
You don't want to configure a toolchain such as Webpack.
You need to distribute a package on NPM.
You require nested CSS support by default (PostCSS is needed).
You want to avoid switching between multiple files.

CSS-in-JS

There are various libraries available for CSS-in-JS. The most popular ones are styled-components and Emotion.

Some libraries like Linaria extract CSS to files at build-time, making it a zero-runtime solution. You can find CSS-in-JS benchmarks. to compare various libraries.

You might use CSS-in-JS if...
You want to share components on NPM.
You want to leverage the JavaScript ecosystem.
You want to co-locate styles with components for easy deletion.
You need to support multiple themes.
You might not use CSS-in-JS if...
You don't want to set up a toolchain like Webpack.
You find it hard to name things.
You believe it violates separation of concerns.
You don't want to enforce a specific CSS-in-JS library for all consumers.

Theme UI / Styled System

Both Theme UI and styled-system are built around the System UI theme specification, which aims to make UI components, libraries, and tools as interoperable as possible.

These libraries are primarily used for implementing component libraries and design systems, with Chakra UI being one of the most popular examples.

You might use Theme UI / styled-system if...
You're building a design system or component library.
You want to enforce a consistent style across your application.
You want to reduce the need for custom CSS.
You want a more declarative approach to styling.
You might not use Theme UI / styled-system if...
You prefer traditional CSS.
You believe that styling should be separated from the component's logic.
You dislike the idea of passing styles as props.

Tailwind CSS

Tailwind CSS has been gaining popularity due to its minimalist approach in contrast to more bloated CSS frameworks like Bootstrap. It also offers an intuitive API that streamlines development.

When combined with Tailwind UI, it is an excellent tool to create scalable and aesthetically pleasing applications.

You might use Tailwind if...
You prefer using HTML or JSX for your styling.
You want to implement a design system.
You want to minimize the time spent naming components.
You might not use Tailwind if...
You are not fond of lengthy class names.
You don't want to learn Tailwind's syntax, which is slightly different from standard CSS.

CSS in React Native and Expo

React Native is a popular choice for cross-platform mobile app development. Expo builds on top of React Native, providing additional tools and services. Styling is done with JavaScript objects instead of CSS.

CSS can be used through a styling system similar to inline styles in React. However, this approach has limitations, including the lack of support for pseudo-classes and pseudo-elements. Nativewind is a library that simplifies styling with a Tailwind-inspired set of utility classes for React Native components. I truly believe that the future of software developmet is in cross-platform development, in React Native, in Expo.

Conclusion

I hope that this comprehensive guide has provided you with a better understanding of the various approaches to styling React applications. By weighing the pros and cons of each method, you can determine which solution best fits your team, application size, and project goals. Remember, there is no one-size-fits-all solution when it comes to styling, and it's essential to consider factors like maintainability, scalability, and team collaboration. If you have any suggestions or feedback, please feel free to leave a comment below or reach out to me on Twitter.

Thanks for reading!