Styling React apps - what are the options?

🕑 5 minute read

When building a React app there are a number of ways you can styles your components and layout. These range from plain CSS classes on elements through to fully integrated style libraries that handle building the styles and creating class names for you.

This is by no means an exhaustive list, but the key areas are:

  • Inline styles
  • CSS
  • Sass build and imported as CSS
  • Sass classes imported as a module
  • Libraries such as Tailwind which are entirely utility class based systems
  • Libraries such as Bootstrap where utility’s and prebuilt components are available
  • Component libraries like ReactBootstrap and MUI with fully built components and properties
  • Style building libraries like Styled Components
  • JavaScript based styles like JSS

Inline styles

One of the original styling mechanisms of the web. Styles are placed directly on an element to style it.

<div style="margin-top: 10px; margin-bottom: 20px;"></div>

In a React component we need to change a few things... First, the styles string becomes an object. Next, the properties need to be converted to the JavaScript versions - margin-top becomes marginTop. The rule of thumb here is that if there is a hyphen in the property name, it's removed and the next letter is capitalised. Finally the values must be either strings or numbers. You can however concatenate these together - 10 + "px" or ${myVar}px

<div style={{
        marginTop: 10 + "px",
        marginBottom: `${myVar}px`
    }}
/>

It's easy to create styles that work for just one element and there is no risk of namespacing clashes - however duplication is an issue, as is the lack of :hover or @media.

CSS

We all know CSS. As with normal HTML applying classes to React components is just as simple - Just add the class name to the className prop of the element instead of class.

/* index.css */

.myClass {
    property: value;
}
import './index.css'; 

// ...

<div className="myClass" />

An advantage of plain CSS is that it is as simple as creating a .css file and importing it (in the App root or main index). The styles can then be used everwhere in your App. However the disadvantage of this global style is that it could have unintended styling side effects, especially if multiple people are working on a big project.

A convoluted example:

span { color: red; }

/* somewhere else */

div span { color: blue; }

/* somewhere else again */

div {
    color: green !important;
}

👆 All the text will be green!

Sass build and imported as CSS

Sass follows the same pattern to CSS, except that you need to update the app to handle sass.

yarn add node-sass
/* index.scss */

.myClass {
    property: value;
}
import './index.scss'; 

// ...

<div className="myClass" />

It has the same advantages, plus the ability to nest code, create mixins (re-usable blocks), functions and split files up. It used to also have an advantage of variables, but CSS now has them too. It also has the same disadvantages!

CSS and Sass classes imported as modules

The biggest disadvantage of both CSS and Sass is the global scope. By using them as modules (supported in Create React App currently) we can completely remove this issue. In CRA you must have a file with *.module.css or *.module.scss as the name. This makes CRA treat the file as a module and it is scoped to just this component that imports and uses it. There is no setup needed, it works out of the box as long as the files are named correctly. You can still mix normal css or sass with the modular version, but it's best to use global scpoed CSS just to reset browser styles and establish the very base of the styles like default fonts.

/* index.module.css */

.myClass {
    property: value;
}

.myOtherClass {
    property: value;
}
import styles from './index.module.scss'; 

// ...

<div className={styles.myClass} />
<div className={styles.myOtherClass} />

When the component renders, the actual classname used is a unique hash so there is no risk of namespace clashes and style conflicts with other modules. Global styles can still affect the modules though!

This convoluted example will mess up your module styles, but it would also do that to all other examples too!

/* index.css - global css styles */
* {
    display: none !important; 
}

Utility class based frameworks

Up until now we have relied on our own styles to style our app. Now we can look at some other options. Utility class frameworks consist classes that just apply to single things like padding left, margin top or backround colour. It's up to the developer to use these Lego bricks to create more complex kits. There have been a number of these over the years, but I want to focus on Tailwind.

Tailwind

A prime example is Tailwind. All the styles have all be abstracted away as classes that can be used in combination to build up more complex components.

There is a little more to configure than some of the other options but once done there is little need to revisit the config.

Add required packages for Tailwind & React

yarn add tailwindcss autoprefixer postcss postcss-cli

Create a postcss config for Tailwind

// file: postcss.config.js
const tailwindcss = require("tailwindcss");

module.exports = {
    plugins: [tailwindcss("./tailwind.config.js"), require("autoprefixer")]
};

Update the build & start scripts to create the index.css

...
"scripts": {
    "build:css": "postcss src/styles/index.css -o src/index.css",
    "watch:css": "postcss src/styles/index.css -o src/index.css -w",
    "start": "run-p start:react",
    "start:react": "npm run watch:css & react-scripts start",
    "build:react": "npm run build:css && react-scripts build",
    "build": "npm run build:react",
    ...
  },
...

Create a config for Tailwind

// tailwind.config.js
module.exports = {
    purge: ["./src/**/*.jsx"],
    darkMode: "media", // or 'media' or 'class'
    theme: {
        extend: {}
    },
    variants: {
        extend: {}
    },
    plugins: []
};

Import the created CSS and use the classes

import "./index.css";

// ...
<div class="w-32 h-32 md:w-48 md:h-auto md:rounded-none rounded-full mx-auto"/>

Once the config is setup, then it's very similar to just using CSS (or Sass), just with a massive amount of prebuilt classes to utilise.

Component frameworks

Some frameworks have extra patterns pre-built for developers to use. They can include forms, media, or popup, prebuilt so a single class or set of classes can setup a component quickly. Bootstrap is probably the most famous, although there are others like Bulma or Foundation.

Bootstrap

Bootstrap is similar to Tailwind in many respects. It's a library that gives a huge amount of customisation to the developer. Although not as wide ranging as Tailwind with it's utility classes, it makes up for with prebuild component classes.

yarn add bootstrap
import 'bootstrap/dist/css/bootstrap.min.css';

// ...

<div className="mt-2 bg-danger text-white" /> 

Component based libraries

As well as libraries, such as Bootstrap, that contain the classes to build the components, there are also libraries that consist of prebuilt components. With these there is no need to add classes. The components already contain the styles needed to display correctly, and usually have props to alter it's variant.

React Bootstrap

React Bootstrap is a replacement for using BootStrap classes directly on elements. eg a <h1 /> is replaced with the <H1 /> component.

yarn add react-bootstrap bootstrap
import 'bootstrap/dist/css/bootstrap.min.css';
import Button from 'react-bootstrap/Button';

// ...
<Button variant="primary">Hello</Button>

Themes and customization can still be carried out by importing the bootstrap sass into your own sass file and using it like the sass/css options above.

Material UI, aka. MUI

Material UI is very similar to React Bootstrap in some ways. Unlike React Bootstrap though there is no CSS to import, but it contains a vast array of prebuilt components and layout wrappers. The configuration for MUI is done in JavaScript and not by overriding the Bootstrap theme.

yarn add @material-ui/core
import { Button } from '@material-ui/core';

// ...

<Button color="primary">Hello!</Button>

To theme Material UI there are a few handy utils like createMuiTheme which alows customisation of core variables like Palette, Spacing, Typography; and and a ThemeProvider context that allows the passing of the theme to child components.

import { createMuiTheme } from '@material-ui/core/styles';

const theme = createMuiTheme({
  palette: {
    primary: {
      main: "#f00",
    }
  },
});

<ThemeProvider theme={theme}>
  <Button color="primary">Hello!</Button>
</ThemeProvider>

Themes can also be nested to allow customisation of a subset of components.

Style building libraries

These contain the raw building blocks for HTML markup and create a wrapper for creating scoped styled components.

Styled Components is the main example of this and is used under the hood by some other libraries.

yarn add styled-components

Styled components is very simple to get up and running:

import styled from 'styled-components'

const Button = styled.button`
    background: #f00;
`

// ...

<Button>Hello</Button>

Here we created a Button component that uses the styled component's styled.button which returns a HTML button element. We then use regular CSS to set the background color between the ` ` . Variables can also be used in styled components like so:

import styled from 'styled-components'

const Button = styled.button`
    background: ${(props) => props.background };
`

// ...

<Button background="#ff0">Hello</Button>

This will use the background prop passed to the component, in this case #ff0. Where you are doing this, it's advisable to use propTypes or similar to set default and required props so there is always a value to use!

JavaScript based styles

Who needs CSS anyway! Let's just use JavaScript for everything!

JSS

JSS is

yarn add react-jss
import {createUseStyles} from 'react-jss'

// ...

// Create a style
const useStyles = createUseStyles({
  myClass: {
    color: '#f0f'
  },
  //  ...
})

const Button = ({children}) => {
    // Get the styles we created
    const classes = useStyles()
    
    // Use them on the button
    return (
        <button className={classes.myClass}>
            {children}
        </button>
    )
}

const App = () => (
    <Button>Hello</Button>
);

As per MUI there is also the ability to theme;

import { createUseStyles, ThemeProvider, useTheme } from 'react-jss'

// ...

// Create a style
const useStyles = createUseStyles({
  myClass: {
    color: '#f0f',
    background: ({ theme }) => theme.background
  },
  //  ...
})

const Button = ({children}) => {
    // Get the styles we created
    const classes = useStyles()
    
    // Use them on the button
    return (
        <button className={classes.myClass}>
            {children}
        </button>
    )
}

const theme = {
  background: "#00f"
};

const App = () => (
 <ThemeProvider theme={theme}>
    <Button>Hello</Button>
  </ThemeProvider>
);

Conclusion

This was never a competition with a winner. All of the options have merits, caveats, pros and cons. I have found that, in my usage at least, each is better suited to different tasks.

BootStrap, React BootStrap and MUI have it all built in and ready to go. You don't need to think about how to build a component, they already exist.

CSS, Sass, JSS and Styled Components are more suited to building the UI from scratch.

Tailwind sits roughly in the middle, with enough wiggle room to have your own styles but still have all the utilities to get something nice looking rapidly.