Styling React Components

Matthew MacFarquhar
5 min readJul 9, 2024

--

Introduction

I have been reading this book on React 18 and its best practices. As I go through this book, I will distill the core pieces of the most interesting tid-bits in this series of articles on React. In this article we will talk about a couple options we can take to style components in React and avoid polluting the global namespace with a bunch of styles which may clash. React offers three solutions for this: Inline styles, CSS modules and the Styled Components library.

Inline Styling

The first option we have is inline styles, this is quite a departure from a conventional separation of concerns line of thinking we have all grown up with that mandates that markup is in an html, logic is in js scripts and styling is in a css file.

However, just as React has pushed back on the logic+markup separation, they have also recommended using inline styles over storing styles in some other css file.

This allows us to do some cool things that we could not do normally if the CSS lived in its own file. For example, dynamically changing styles based on a component state like below.

const FontSizeChangingInput = () => {
const [fontSize, setFontSize] = useState(16);

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setFontSize(Number(e.target.value));
}

return (
<input
onChange={handleChange}
value={fontSize}
style={{fontSize: fontSize}}
/>
)
}

If you wanted to emulate this, you would need to manually update the DOM node’s fontSize style property on each change. In this case inline React styles are at a huge advantage!

However, inline styles do not allow pseudo-selector like :hover or media queries, so if you need some of those functionalities you would have to hack together a work around.

Another difficult problem with inline styles is that they make it hard to debug styling bugs, since each inline has its own style object we cannot use dev tools to easily change all the styling of a shared class at once.

CSS Modules

CSS modules are an option to write styles in a .css file BUT keep them locally scoped instead of global by transforming the readable styling class name into a hash which is unique across all class names in our project.

To use CSS modules, we need to set up Webpack and add some css and style loaders.

const HtmlWebpackPlugin = require('html-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const path = require('path');
const isProduction = process.env.NODE_ENV === 'production';

module.exports = {
entry: './src/index.tsx',
devtool: !isProduction ? 'source-map' : false, // only create source map for dev
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
sourceMapFilename: '[name].[contenthash].map',
chunkFilename: '[id].[contenthash].js',
publicPath: '/'
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json', '.css']
},
target: 'web',
mode: isProduction ? 'production' : 'development',
module: {
rules: [
{
test: /\.(tsx|ts)$/,
exclude: /node_modules/,
use: {
loader: 'ts-loader',
options: {
transpileOnly: true
}
}
},
{
test: /\.css$/,
use: [
{
loader: 'style-loader'
},
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[local]--[hash:base64:5]'
}
}
}
]
}
]
},
plugins: [
new ForkTsCheckerWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Your Project Name',
template: './src/index.html',
filename: './index.html'
})
],
optimization: {
splitChunks: {
cacheGroups: {
default: false,
commons: {
test: '/node_modules/',
name: 'vendor',
chunks: 'all'
}
}
}
}
}

This Webpack has instructions for .module.css files to load them up and name the classes with the local className, two dashes and then a hash, this makes it much easier to find and debug our css classes from DevTools.

We need to declare the .css file as a type of importable file.

declare module '*.css' {
const content: Record<string, string>
export default content
}

And we will create our style sheet.

.background-red {
background-color: #ff0000;
}

.button {
composes: background-red;
width: 320px;
padding: 20px;
border-radius: 5px;
border: none;
outline: none;
}

.button:hover {
color: #fff
}

.button:active {
position: absolute;
top: 2px;
}

@media (max-width: 480px) {
.button {
width: 160;
}
}

We can do everything we can in a regular style sheet AND we have an added feature of composable classes where we can build up css classes by inheriting the styles of other ones.

Now, in our entry file (index.tsx), we can directly import and use our style module like so.

import { createRoot } from 'react-dom/client';
import * as styles from "./styles.css";

const Button = () => (
<button className={styles['button']}>Click Me!</button>
)

createRoot(document.getElementById('root') as HTMLElement).render(
<Button/>
)

There is definitely a lot more overhead to get CSS modules working when compared with inlined components, BUT we get the full power of CSS and we can get some useful class names generated by Webpack to help debugging.

Unfortunately we have lost the dynamic style updates based on state that we had with inlined components.

Styled Components

Our last option is to use the styled components library. This library allows us to create styled DOM objects as JS objects and then use them in our code. We can use template strings to define the style which allows us to inject JS logic into the styles like we had done with inlined styling.

const Button = styled.button`
background-color: #ff0000;
width: 320px;
padding: 20px;
border-radius: 5px;
border: none;
outline: none;
&:hover {
color: #fff;
}
&:active {
position: relative;
top: 2px;
}
@media (max-width: 480px) {
width: 160px;
}
`;

export const Example = () => {
return (
<>
<Button>Styled Button</Button>
</>
)
}

With this approach we get

  • JS injected styling props
  • The full power of CSS (e.x. pseudo-elements and media queries)
  • A shared className amongst the DOM nodes created using the styled component

The only major thing we miss that CSS modules provides is the ability to inject local readable class names with ease (Note: the styled-components library claims they can actually support this using babel https://styled-components.com/docs/tooling#babel-plugin but it was not as easily configurable as setting it up for CSS modules was).

My only other issue with the styled-component library is that that writing CSS in a template string is annoying, mostly because my IDE will not flag bugs I may be writing.

Conclusion

In this article, we explored three different ways to style react components. In my opinion, the obvious winner is the styled-components library which offers the JS interpolated styles of inline components + the full scope of CSS features we get by using CSS modules and shared classNames amongst like components. My only — small — gripe is the UX for writing these styled-component CSSs in a string is annoying.

--

--

Matthew MacFarquhar
Matthew MacFarquhar

Written by Matthew MacFarquhar

I am a software engineer working for Amazon living in SF/NYC.

No responses yet