Editor’s note: This article was last updated by Emmanuel Odioko on 31 May 2024 to cover using React Hooks like useState
and useEffect
to implement dynamic styling to accomplish things like switching themes and responsive layouts.
So, you’re here to learn about styling in React? Well, you’ve come to the right place. In this article, we will review a variety of methods for styling React components, including inline styling, using styled-components, CSS Modules, and more. By the end of this guide, you’ll be ready to pick the best styling option for your project.
Check out this video tutorial for more information:
Four ways to style in React
Learn about styling in React using CSS modules, inline styling, styled-components, and Tailwind CSS with Real Tough Candy. You can find the original blog post here on the LogRocket blog: https://blog.logrocket.com/the-best-styling-in-react-tutorial-youve-ever-seen-676f1284b945/?youtube-tutorial 00:00 Intro 00:59 What is inline styling in React? 03:20 Creating a style object variable 04:43 Should I use styled-components in React?
We will use Create React App (CRA) to set up a demo React application. Here is a demo of the app:
Now, let’s jump into our first styling method.
If you are familiar with basic HTML, you’ll know that it is possible to add your CSS inline. This is similar in React.
We can add inline styles to any React component we want to render. These styles are written as attributes and are then passed to the element. Let’s style parts of our component using inline styles:
/** src/todo/AddTodo.js **/ //...code omitted for brevity const AddTodo = () => { //... return ( <div style={{ display: "flex", flexDirection: "column" }}> <h2 style={{ padding: "10px 20px", textAlign: "center", color: "white" }}> TODO </h2> <div style={{ display: "flex", justifyContent: "center", alignItems: "center" }} > <label style={{ padding: "10px 20px", textAlign: "center" }} htmlFor="new-todo" > What needs to be done? </label> </div> <div style={{ display: "flex", justifyContent: "center", alignItems: "center" }} > <form onSubmit={handleSubmit}> <input onChange={onChange} value={task} ref={inputRef} /> <button>Add </button> </form> </div> {message && ( <div style={{ display: "flex", justifyContent: "center", alignItems: "center" }} > <h4 style={{ color: "red" }}>{message}</h4> </div> )} </div> ); }; export default AddTodo;
In the code above, we added inline styles to the HTML elements in the AddTodo()
. There are a few important things to note about this method.
First, there are two curly brackets. What we are rendering is written in JSX, and for pure JavaScript expressions to be used in JSX, they have to be included in curly brackets. The first curly bracket injects JavaScript into JSX. The inner curly brackets create an object literal. The styles are then passed as object literals to the element.
N.B., JSX is a preprocessor step that adds XML syntax to JavaScript. You can use React without JSX, but JSX makes React a lot more elegant. Just like XML, JSX tags have a tag name, attributes, and children.
The next thing to note is that the properties are separated by a comma. This is because what we are passing is an object. Because it is a JavaScript attribute, the attributes are written in camelCase and not separated by a dash.
In the code above, we just added a few properties to the elements we styled. But what if we had many more styles to add to the element? This is where the inline method breaks down because it will no longer look clean.
There is a way around this, though. We can create object variables and pass them to the elements.
We create a style object variable the same way we create a JavaScript object. This object is then passed to the style attribute of the element we want to style.
So, instead of adding the styles inline directly, like we did in the previous example, we can pass the object
variables as you see below:
/** src/todo/AddTodo.js **/ const AddTodo = () => { //... return ( <div style={Container}> <h2 style={Header}>TODO</h2> <div style={LabelContainer}> <label style={Label} htmlFor="new-todo"> What needs to be done? </label> </div> <div style={FormContainer}> <form onSubmit={handleSubmit}> <input onChange={onChange} value={task} ref={inputRef} /> <button>Add </button> </form> </div> {message && ( <div style={Message}> <h4 style={{ color: "red" }}>{message}</h4> </div> )} </div> ); }; const Container = { display: "flex", flexDirection: "column" }; const Header = { padding: "10px 20px", textAlign: "center", color: "white" }; const LabelContainer = { display: "flex", justifyContent: "center", alignItems: "center"}; const Label = { padding: "10px 20px", textAlign: "center" }; const FormContainer = { display: "flex", justifyContent: "center", alignItems: "center"}; const ErrorMessage = { display: "flex", justifyContent: "center", alignItems: "center"};
In the code above, we created six object variables: Container
, Header
, LabelContainer
, Label
, FormContainer
, and ErrorMessage
. Then, we passed these variables to the element instead of typing them directly.
N.B., we did not have to use double curly brackets in the element because these variables are objects themselves.
If you look at the object properties, the camelCases will be converted to dash-separated CSS attributes during compilation. Here is an example:
backgroundColor: "#44014C", minHeight: "200px", boxSizing: "border-box" In plain CSS, these will be written as: background-color: #44014C; min-height: 200px; box-sizing: border-box;
The camelCase to dash-separated string change applies only to the property names, not the property values. However, it is possible to pass a variable as a value to a property. We can do this:
/* in .js file */ const spacing = "10px 20px"; const Header = { margin: spacing, padding: spacing // ... }
In many JavaScript environments, creating a global object variable may be bad practice, but it’s OK in React. Because files are not visible to other files unless imported, we can create as many object variables as we want — even with the same name — without conflict.
The style objects and components do not have to be in the same file. We can create a separate .js
file for our styles, export these styles, and then import them into the component where we want to use them. Doing this makes styles reusable across multiple components. Let’s do this for our component.
First, we’ll create a separate .js
file called styles.js
. Then, we’ll add the following styles:
const Container = { display: "flex", flexDirection: "column" }; const Header = { padding: "10px 20px", textAlign: "center", color: "white" }; const LabelContainer = { display: "flex", justifyContent: "center", alignItems: "center" }; const Label = { padding: "10px 20px", textAlign: "center" }; const FormContainer = { display: "flex", justifyContent: "center", alignItems: "center" }; const ErrorMessage = { display: "flex", justifyContent: "center", alignItems: "center" }; export const styles = { Container: Container, Header: Header, LabelContainer: LabelContainer, Label: Label, ErrorMessage: ErrorMessage, FormContainer: FormContainer } export const styles = { Container: Container, Header: Header, LabelContainer: LabelContainer, Label: Label, ErrorMessage: ErrorMessage, FormContainer: FormContainer }
In the code above, we can export each style object individually, which will also mean importing them individually. That might get tedious if there are many style objects in the file.
Therefore, creating an object that contains all styles makes more sense. This object will be exported and imported once to the component where it will be used. Let’s do this:
/** AddTodo.js file **/ // Import the styles import {styles} from "./styles"; const AddTodo = () => { //.... return ( <div style={styles.Container}> <h2 style={styles.Header}>TODO</h2> <div style={styles.LabelContainer}> <label style={styles.Label} htmlFor="new-todo"> What needs to be done? </label> </div> <div style={styles.FormContainer}> <form onSubmit={handleSubmit}> <input onChange={onChange} value={task} ref={inputRef} /> <button>Add </button> </form> </div> {message && ( <div style={styles.ErrorMessage}> <h4 style={{ color: "red" }}>{message}</h4> </div> )} </div> ); };
Above AddTodo()
, we’ll import the styles
object. This object is then used to style the components of our React app and is used just like any JavaScript object.
The thing to understand from this method is that styles can be used and reused in multiple components, they just need to be imported and added to the style attribute. Using inline styling can help you quickly prototype your interface.
Of course, there are situations where you should not use inline styling. For example, the styles
declaration can quickly get messy and disorganized with inline styling. Additionally, this method does not encourage the DRY (Don’t Repeat Yourself) principle. Using inline styling is not suitable for large projects with a lot of code.
This is where our next two methods come in.
With styled-components, we can write actual CSS in our JavaScript files. This allows us to use all the features of CSS — like media queries, pseudo-selectors, nesting, and more — directly within our JavaScript.
styled-components use ES6’s tagged template literals to style components. This approach removes the mapping between components and styles, meaning that when you define your styles, you’re actually creating a normal React component with your styles attached to it.
With styled-components, we can create reusable components with styles. To get started, install styled-components by running $ npm install --save styled-components
in your React app’s directory.
Next, we’ll incorporate styled-components into our to-do app. Import the styled-components
package in your AddTodo.js
file with import styled from 'styled-components';
.
Now, we can start using it right away. We’ll first create a styled-component, and then see how to use it:
/** AddTodo.js file **/ const Container = styled.div` display: flex; flex-direction: column; `; const Header = styled.div` padding: 10px 20px; text-align: center; color: white; `; //....
In the code above, we created a component that can be used the same as any React component. However, notice that we are using pure CSS in a JavaScript file.
Next, let’s put this component:
/** AddTodo.js file **/ function AddTodo() { //... return ( <Container> <Header>TODO</Header> <LabelContainer> <Label htmlFor="new-todo">What needs to be done?</Label> </LabelContainer> <FormContainer> <form onSubmit={handleSubmit}> <input onChange={onChange} value={task} ref={inputRef} /> <button>Add </button> </form> </FormContainer> {message && ( <ErrorContainer> <ErrorMessage>{message}</ErrorMessage> </ErrorContainer> )} </Container> ); }
In the code above, we used the styled-component we created and replaced the HTML tags with the defined component style tags. The styled-component is used like any other HTML element. The only difference is that it comes with its own predefined styles. You can access the code in the CodeSandbox playground.
styled-components dynamically style your elements in whichever way you deem fit. They encourage the DRY principle with great patterns for organizing your code, and they have compatibility with a wide range of frameworks and libraries. They are also great for developing and maintaining design systems.
However, styled-components come with a large computing overhead when converting declarations to plain CSS. This can affect the performance of your application. styled-components also require time to get familiar with the syntax and procedures.
If you are looking for an alternative for styled-components, check out Emotion. Next, let’s discuss a third way to style in React.
A CSS Module is a CSS file in which all class and animation names are scoped locally by default. Take note of the words “scoped locally.”
CSS class names and animation names are scoped globally by default. This can lead to conflict, especially in large style sheets, because one style can override another. CSS Modules solve this problem by scoping CSS classes locally, ensuring they are only available within the component where they are used.
A CSS Module is essentially a .css
file that is compiled. When compiled, it produces two outputs. One is CSS, a modified version of input CSS with the renamed class names. The other is a JavaScript object that maps the original CSS name with the renamed name.
Now, let’s create a CSS class in a module for a sample error message. The name of our module is styles.module.css
:
.error-message { color: red; font-size: 16px; }
When compiled, it will produce something like this:
.error-message_jhys { color: red; font-size: 16px; }
The added jhys
is a sample key (that I added myself) that is used to uniquely identify this class. As mentioned earlier, it produces a JavaScript object, which can be imported into the React file and used like so:
{ error-message: error-message_jhys }
Now, let’s see how we can use it:
/** .js file **/ import styles from './styles.css'; function Message() { return ( <div className={styles.ErrorMessage}>I am an error message</div> ); }
CSS Modules can easily integrate with CSS or SCSS styling engines. They generate unique class names that are void of conflicts and have built-in support in React. CSS Modules also fix global scope issues for CSS declarations.
However, referencing class names with CSS Modules can oftentimes be confusing.
Tailwind CSS offers a different approach in which no CSS needs to be written to style a React application. Instead, Tailwind CSS uses utility classes for each CSS property that you can use directly in your HTML or JSX.
Perhaps you are wondering that if each CSS attribute is mapped to a class, what is the bundle size for the final CSS? The bundle size is actually very small, with most projects shipping bundle sizes lower than 10kB. But how? Tailwind CSS processes the classes you use during build time and builds a CSS bundle tailored to your project.
Tailwind CSS offers much more than CSS classes mapped to attributes; it’s also a complete framework that supports responsive behavior, grids, states, dark mode, and more. In addition, it’s highly configurable.
To set up Tailwind CSS on a CRA project, a few steps (and libraries) are involved. This is because of what it requires to modify the application’s build process to generate the CSS bundles.
First, begin by installing Tailwind CSS and generating tailwind.config.js
and postcss.config.js
. Then, run the $ npm install -D tailwindcss postcss autoprefixer
and $ npx tailwindcss init -p
commands.
Note that all Tailwind CSS-related libraries are installed as dev
packages, meaning that they won’t affect your JavaScript bundle size.
In your tailwind.config.js
file generated above, add the following value to the content
array:
/** @type {import('tailwindcss').Config} */ module.exports = { content: ["./src/**/*.{js,jsx,ts,tsx}",], theme: { extend: {}, }, plugins: [], }
Now, we need to set up our CSS baseline. Because Tailwind CSS uses multiple classes with relative values, it is important to set up the same CSS style base across all browsers. Tailwind CSS comes with some default styling to do just that.
To achieve this, navigate to your src/index.css
file and paste the following code:
/* ./src/styles.css */ @tailwind base; @tailwind components; @tailwind utilities;
N.B., importing these classes will make all your default elements behave differently than what you expect, so elements such as headings and paragraphs will have the same styling properties until you add Tailwind CSS classes to them.
Tailwind CSS offers many configuration options during its setup, so if you want to learn more, check out the official documentation.
With your setup ready, you can now use CSS classes directly on your project. Build a ToDo
component using Tailwind CSS:
/** AddTodo.js file **/ function AddTodo() { //... return ( <div className="flex flex-col"> <h2 className="px-2.5 py-5 text-center text-white">TODO</h2> <div className="flex justify-center items-center"> <label className="px-2.5 py-5 text-center" htmlFor="new-todo"> What needs to be done? </label> </div> <div className="flex justify-center items-center"> <form onSubmit={handleSubmit}> <input onChange={onChange} value={task} ref={inputRef} /> <button>Add </button> </form> </div> {message && ( <div className="flex justify-center items-center"> <h4 className="text-red-800">{message}</h4> </div> )} </div> ); }
Notice that these classes are directly injected as text to the className
prop and that there is a complete reference to all the class names with states and responsive attributes that can be used.
Tailwind CSS is easy to use for customizing elements and building clean user interfaces. Tailwind CSS offers a huge reduction in writing custom CSS code, which allows you to develop UI screens faster.
However, Tailwind CSS often requires you to implement it from scratch, as basic components like buttons, navbars, and tabs are not provided. Additionally, learning Tailwind CSS will require a minimal learning curve.
Sass (Syntactically Awesome Style Sheets) is a CSS preprocessor with many features for creating reusable styles, functionalities for nesting, and organizing CSS declarations. Using Sass or plain CSS is based on writing styles in an external file and importing it into the component that needs it.
In summary, Sass is all about writing standard CSS with added benefits. You can also write your styles using Sass or SCSS syntax.
This guide uses the SCSS syntax to demonstrate its usage in the to-do application. To get started, add the node-sass
dev dependency to your project with the npm install node-sass
command. Also, update your .css
file to a .scss
file extension.
In the code below, you will see a mixture of CSS and SCSS syntax. The SCSS engine permits writing plain CSS while also leveraging its additional features:
/** todo.scss file **/ // declare global variables $paddingVertical: 10px; $paddingHorizontal: 20px; $text-center: center; $text-white: #ffffff; $text-black: #000000; $text-red: rgb(185 28 28); $font-size: 16px; .error { color: $text-red; } .container{ display:flex; flex-direction: column; } .flex-center { display: flex; justify-content: center; align-items: center; } %typo-large { padding: $paddingVertical $paddingHorizontal; text-align: $text-center; } .h2 { @extend %typo-large; color: $text-white; } .label { @extend %typo-large; }
Next, import the styles declaration into your component. So, in the AddTodo
file, import the contents of the todo.scss
files. Then you can access your styles as depicted below:
/** AddTodo.js file**/ //...imports omitted for brevity // Import the styles import "./todo.scss"; function AddTodo() { //...code omitted for brevity return ( <div className="container"> <h2 className="h2">TODO</h2> <div className="flex-center"> <label className="label" htmlFor="new-todo"> What needs to be done? </label> </div> <div className="flex-center"> <form onSubmit={handleSubmit}> <input onChange={onChange} value={task} ref={inputRef} /> <button>Add </button> </form> </div> {message && ( <div className="flex-center"> <h4 className="error">{message}</h4> </div> )} </div> ); }
Here is the CodeSandbox playground for the above example.
Sass offers a wide range of features for dynamically styling user interfaces and has a large community of users and support. It also integrates easily with standard CSS code.
Unfortunately, learning Sass can be challenging due to its steep learning curve, and it requires effort to properly structure and organize your SCSS code.
Dynamic styling is seen in almost every application, but in case it’s new to you, it is the ability to change the appearance of an application based on a user’s interaction. Popular use cases include theme switching, responsive layouts, and interactive animation. In React, this can be accomplished using React Hooks.
React provides us with Hooks like useState
and UseEffect
, which are primarily used for state management and side effects in functional components. However, they can also be used for dynamic styling.
Because most developers dislike the light mode and find it annoying to code with it, let’s take a look at this example of theme switching in VS Code:
import React, { useState } from 'react'; import './App.css'; const App = () => { const [theme, setTheme] = useState('light'); const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light'); return ( <div className={`app ${theme}`} style={{height: '100vh', width: '100vw'}}> <button onClick={toggleTheme}>Toggle Theme</button> <p>LogRocket</p> </div> ); }; export default App; CSS .light { background-color: white; color: black; } .dark { background-color: black; color: white; }
When you click the Toggle Theme below, you get to toggle between two backgrounds:
In this example, we demonstrated how to use the useState
and useEffect
React Hooks to implement an interactive and responsive interface that switches between a light and dark mode.
Using the useState
and useEffect
Hooks also helps ensure your component adapts to different screen sizes, whether on mobile or desktop.
useState
keeps track of the current window width, and when the window is resized, useEffect
is used to update the width. This is similar to using an event listener in vanilla JavaScript for window resizing.
There’s a more appropriate way to do this using the ResizeObserver
API, but this method of responsiveness is still relevant. Let’s see how it works:
import React, { useState, useEffect } from 'react'; const ResponsiveComponent = () => { const [windowWidth, setWindowWidth] = useState(window.innerWidth); useEffect(() => { const handleResize = () => setWindowWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); const isMobile = windowWidth < 768; return ( <div style={{ padding: isMobile ? '10px' : '20px', backgroundColor: isMobile ? 'lightblue' : 'lightgreen', }}> <p>{isMobile ? 'Mobile View' : 'Desktop View'}</p> </div> ); }; export default ResponsiveComponent;
Dynamic styling makes an application more engaging and responsive. But keep in mind that dynamic styles introduce a high possibility of future bugs. This is because there are several factors involved when an application has multiple components, such as managing state changes, conditional rendering, and ensuring consistency across those components.
In this guide, we have explored the pros and cons of various styling approaches, noting that the cons can lead to poor performance, especially in large projects. Keep these tips in mind when styling:
Most of the performance overhead can be traced to bulky projects, so if you have a light project, you will probably avoid these issues.
While many libraries and CSS engines provide ways for styling in your React application, it is important to note that there is not a one-size-fits-all approach to choosing the best React styling solution.
When looking at the best way to style a React application, several factors vary in different software teams. Here are a few considerations:
These are some of the questions that you will need to answer before deciding the best way to style in React. Ultimately, the team plays a major role in selecting a CSS solution that best fits a product requirement while considering the expertise and domain knowledge of the team.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowValidating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
Optimize search parameter handling in React and Next.js with nuqs for SEO-friendly, shareable URLs and a better user experience.
Learn how Remix enhances SSR performance, simplifies data fetching, and improves SEO compared to client-heavy React apps.