DEV Community

Aki Rautio
Aki Rautio

Posted on • Originally published at akirautio.com

Different ways of writing styles in React

Writing CSS styles is essential for frontend applications, but CSS doesn't scale well. The lack of namespaces or types makes writing styles for complex applications error-prone. Luckily multiple solutions resolve these issues and work well with React.

All the solutions take a bit different approaches to make CSS easier to maintain and have a different set of features and drawbacks. Thus why selecting a suitable solution will enable you to write better code.

Note: Many of these libraries work with other frontend frameworks too but this article focuses on libraries that work with React.

CSS files

The classical way of writing styles for the React application is to write CSS files and use them with Javascript.

The solutions that use CSS files are rarely limited to React since the concepts are universal and the connection between CSS and React is the class name.

CSS files / Inline CSS

Writing plain CSS files or inline CSS for React doesn't differ much from writing them for HTML files. The greatest difference is that to use className property instead of class.

// File: styles.css

.mainClass {
  border: 1px solid blue;
}
.errorClass {
  border: 1px solid red;
}
Enter fullscreen mode Exit fullscreen mode
// File: App.ts

import 'styles.css';

function App({ error }: AppProps){

  return (
    <div className={error ? "errorClass" : "mainClass"} style={{ color: 'red' }}>Main Activity</div>
  )
}
Enter fullscreen mode Exit fullscreen mode

This is a very barebone way to write CSS and it aligns strongly with the way CSS is used in a normal HTML page. Plain CSS enables an easy way to use the same styles between applications regardless of the framework and the CSS files are usable immediately.

The downsides of using only CSS or inline CSS are the ones we mentioned earlier. Your React code doesn't know whether the particular class name exists and CSS is lacking namespacing so you can easily override the previous class. The whole process of providing CSS is also manual so there is no automated merging or splitting of CSS files.

Using plain CSS files works well for reasonably small websites or applications where complexity doesn't get high and the styles are needed to be shared between different frameworks (or just with HTML). In React I would suggest using CSS files through CSS modules if the build system includes the feature.

SASS / LESS

SASS and LESS are preprocessors for CSS. They offer a programmatical approach to writing styles which will be turned into standard CSS.

Using SASS and LESS works very much the same way as normal CSS and the difference only comes when bundling the code.

// File: styles.scss

$normal-border: blue;
$error-border: red;

.mainClass {
  border: 1px solid $normal-border;
}
.errorClass {
  border: 1px solid $error-border;
}
Enter fullscreen mode Exit fullscreen mode
// File: App.ts

import 'styles.scss';

function App({ error }: AppProps){

  return (
    <div
      className={error ? "errorClass" : "mainClass"}
      style={{ color: 'red' }}
    >
      Main Activity
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

The advantage of using either of the preprocessor is that a lot of repetitive styles can be automated (see an example from the common component post). Adding variables or creating iterative loops makes it easy to write more complex classes without writing repetitive content manually.

Since the preprocessor resolves the issue of creating programmatical styles, it might cause even more issues since you can easily use class names in React that do not exist.

I would use SCSS or LESS when there is a need to create programmatical styles (ie. having different class names or having a need to calculate values or colors for the classes). When using a preprocessor, one should someway to test out that the classes exist and work as expected.

CSS modules

CSS modules couple CSS styles more tightly to React and at the same time solves the namespace issue. When a CSS file is imported to React, it will create a namespace for the file.

The import gives an ability to connect the created namespace with the original one by returning an object with original class names as a key.

// File: styles.css

.mainClass {
  border: 1px solid blue;
}
.errorClass {
  border: 1px solid red;
}
Enter fullscreen mode Exit fullscreen mode
// File: App.ts

import styles from 'styles.css';

function App({ error }: AppProps){

  return (
    <div
      className={error ? styles.errorClass : styles.mainClass
      style={{ color: 'red' }}
    >
      Main Activity
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Depending on the connection between React and CSS files allows safer use of class names and makes recognition of the missing classes one step easier than using just plain CSS files. It's also good to note that CSS modules work with any preprocessing library like SASS or LESS.

There are no real drawbacks to using CSS modules as is but it inherits the downsides of writing plain CSS. It naturally lacks the type checks and build-time checks whether the class exists.

Using CSS files with CSS modules protects against complexity which makes it a viable option to use with more complex Javascript applications.

CSS-in-JS

CSS in JS libraries move styles to Javascript files instead of handling them in a separate CSS file. The advantage is to keep all the logic within Javascript instead of splitting the logic between JS and CSS.

Styled components / Emotion

Styled components are one of the first ones that introduced CSS-in-JS and have been one of the most popular ones to use. Emotion is another popular choice.

Both libraries use the styled function that takes an HTML tag and the styles through template literals and returns a React component that creates an HTML element with the generated class name and CSS styles linked to that generated class name.

// File: App.ts

import styled from 'styled-components';

const Content = styled('div')<{ error: boolean }>`
  border: 1px solid ${props => error ? props.theme.errorBorderColor: props.theme.borderColor};
`

function App({ error }: AppProps){
  const theme = {
    mainBorderColor: 'blue',
    errorBorderColor: 'red
  }

  return (
    <ThemeProvider theme={theme}>
      <Content
        error={error}
        style={{ color: 'red' }}
      >
        Main Activity
      </Content>
    </ThemeProvider>
  )
}

Enter fullscreen mode Exit fullscreen mode

The biggest advantage of styled function (and CSS-in-JS in general) is automated naming of classes and handling of CSS files. Using styled functions also gives a lot of freedom to write your style-related logic the way you want (see Common component examples). The styles can be more dynamic and passing specific values to components is easier.

The dynamic behavior of the styled function is also a drawback due complexity of creating static class names. This needs computing power on runtime which may end up leading to slowness. While styled components include server-side rendering, complex styles are still slower to create than static styles.

Styled components and emotion work well with an application that has a lot style related business logic (colors depend on the logic) and it excels more with applications that need dynamic styling.

Vanilla Extract

Vanilla Extract brings CSS-in-JS more to the traditional side. The styles are defined in a typescript file but they are separated from the rest of the application logic. While it also supports passing dynamic content, it's done often by variants and there are no full dynamics. This results that Vanilla Extract can generate styles statically and enable zero runtime need.

// File: styles.css.ts

import { style } from '@vanilla-extract/css';

export const [themeClass, vars] = createTheme({
  color: {
    mainBorder: 'blue'
    errorBorder: 'red'
  },
});

const base = style({
  border: '1px solid'
});

export const mainClass = styleVariants({
  main: [base, { background: vars.color.mainBorder }],
  error: [base, { background: vars.color.errorBorder }],
});

Enter fullscreen mode Exit fullscreen mode
// File: App.ts

import { mainClass } from './styles.css.ts';

function App({ error }: AppProps){

  return (
    <div
      className="${mainClass[error ? 'error' : 'primary']}"
      style={{ color: 'red' }}
    >
      Main Activity
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

A big advantage of Vanilla Extract is the type safety that enables autocomplete in VSCode and ensures that CSS is always correct. and eases up selecting the correct option for a style parameter.

Another key feature with Vanilla Extract is generating CSS during build time instead of runtime. This can be either upside or downside depending on how much dynamic styling is needed. Vanilla extract offers using variants so there are some possibilities but they are very limited compared to styled components.

The key drawbacks come from being strictly build-time. The development flow feels a lot more similar to writing plain CSS files than writing CSS-in-JS which might some people. Vanilla Extract also restricts writing some dependent styles (for a good reason) which might cause issues in case the application needs these.

Vanilla Extract works well in applications where the performance is important and styles are only used within React. If the codebase uses Typescript, it would make a lot of sense to use Vanilla Extract instead of CSS files with CSS modules.

Utility libraries like Tailwind CSS

Utility libraries like TailwindCSS reduce the number of CSS styles needed to be written having commonly used styles abstracted to class names and using those class names to define the style of the HTML element. This keeps the class name size small which helps to keep the CSS file small especially when combined with postprocessor which excludes nonexisting class names.

function App({ error }){
  return (
    <div
      className={["border-2","border-solid",error ? "border-red" : "border-blue].join(" ")}
      style={{ color: 'red' }}
    >
      Main Activity
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

While this is often seen as a tool to do only rapid prototyping, in my experience they are also usable in a real product, especially when combined with custom styles. Utility styles enable to keep the styling inside Javascript files and still not combine CSS in JS.

The downside of the utility libraries is the naming of the new abstraction layer. Since all styles will be written with the new naming, it takes some time to be efficient. The utility libraries also cover only the usual scenarios which might be limiting.

Utility libraries are somewhat middle ground between CSS-in-JS and plain CSS so they fit well in applications where styles are not handling something very unusual. Application with forms or tables would be a great use case, collaborative drawing application most likely not.

Discussion (0)