Styling in React page

Learn about different ways to apply CSS styles in React applications.

Overview

There is no prescribed approach for styling React apps. It’s entirely possible to style components using regular CSS. This approach works fine for many applications, especially smaller ones.

However, as your application grows in complexity, you might encounter challenges with style management. For instance, CSS written for one component can unintentionally affect other parts of your application.

The community has built several styling options. We will be using CSS Modules as it is one of the most popular styling libraries and is very approachable for anyone that is used to styling in plain CSS. CSS Modules offer a way to write CSS that’s scoped to individual components, thus preventing style clashes and maintaining a cleaner codebase.

Objective 1: Applying style with CSS Modules

In this section, we will:

  • Review how plain CSS works in React applications
  • Set up CSS Modules
  • Create a component-specific stylesheet
  • Apply styles to a component

Reviewing how plain CSS works

In a standard React application using regular CSS, styles are global. This means that any style you define can potentially affect any element in your application that matches the style’s selector.

Let’s imagine that we have two components in our application: <BlogPost> and <UserProfile>. Both of them feature an avatar image, one for the author of a post and the other for the signed-in user.

Here’s what the BlogPost.css file might look like:

.avatar {
  float: left;
  margin-right: 10px;
}

And the BlogPost.tsx component:

import React from 'react';
import './BlogPost.css';

const BlogPost: React.FC = ({ authorAvatar, content }) => {
  return (
    <article>
      <img
        alt="Headshot showing…"
        className="avatar"
        src={authorAvatar}
      />
      <p>{content}</p>
    </article>
  );
}

Here’s what the UserProfile.css file might look like:

.avatar {
  border-radius: 50%;
  height: 100px;
  width: 100px;
}

And the UserProfile.tsx component:

import React from 'react';
import './UserProfile.css';

const UserProfile: React.FC = () => {
  return (
    <img
      alt="User avatar showing…"
      className="avatar"
      src="user-profile.jpg"
    />
  );
}

What will the final CSS be?

In this case, the two style declarations combined will be applied to both images:

.avatar {
  border-radius: 50%;
  float: left;
  height: 100px;
  margin-right: 10px;
  width: 100px;
}

This is no good! When someone edits one component, it looks like they’re only changing the styles for that single component, but in fact they are changing any instance where that class name is used globally throughout the entire application.

Let’s solve this!

Scoped stylesheet

CSS Modules works like regular CSS, but the class names are randomized to prevent conflicts between components. When using CSS Modules, if multiple components implement a .avatar class, those classes will be unique and conflict-free because CSS Modules will rename each class with a unique random string like .avatar_R8f2.

For example, these styles in a CSS module:

/* BlogPost.css */
.avatar {
  float: left;
  margin-right: 10px;
}

Will be converted into the following:

/* BlogPost.module.css */
.avatar_R8f2 {
  float: left;
  margin-right: 10px;
}

The standard procedure with CSS Modules is to create a stylesheet for each component. That stylesheet is placed in the same folder as the component. In order to identify the file as a CSS Module, the filename should end with .module.css. The file structure for a BlogPost component will look like the following:

  • BlogPost
    • BlogPost.module.css
    • BlogPost.test.tsx
    • BlogPost.tsx
    • index.ts

Note that CSS Modules cannot rename HTML tags, so all of your styling should use classes to avoid unexpected styling bugs. The following will apply to all img elements in the project and should be avoided.

/* Don't do this with CSS Modules as tag names can not be randomized */
img {
  float: right;
}

Importing and applying styles

During the build process, the CSS classes will be randomized and added to a global stylesheet. The randomized class names are imported into components as an object where the key is the original class and the value is the new, randomized string.

Here’s an example of what the JS object imported from BlogPost.module.css might look like:

{ avatar: "avatar_R8f2" }

In order to add the .avatar class to a component, import the CSS Modules styling object and apply the class using the original class name as the key. Note the HTML class attribute is renamed to className in JSX because class is a reserved word in JavaScript.

import React from 'react';
import styles from './BlogPost.module.css`

const BlogPost: React.FC = ({ authorAvatar, content }) => {
  return (
    <article>
      <img
        alt="Headshot showing…"
        className={styles['avatar']}
        src={authorAvatar}
      />
      <p>{content}</p>
    </article>
  );
}

Exercise 1

Now that we've learned to apply styling in React with CSS Modules, it's time to practice by styling a link in the Home component. You'll bring in a Link component from React Router.

Install CSS Modules

Already done! Vite, our build tool, ships with first-class CSS Modules support. Vite also supports a few other styling options out of the box.

Setup

We've created some CSS for this exercise. Create a CSS Modules file and copy the following styles into it.

✏️ Create src/pages/Home/Home.module.css and update it to be:

a {
    display: inline-block;
    margin-bottom: 0;
    font-weight: normal;
    text-align: center;
    vertical-align: middle;
    touch-action: manipulation;
    cursor: pointer;
    background-image: none;
    border: 1px solid transparent;
    white-space: nowrap;
    padding: 6px 12px;
    font-size: 14px;
    line-height: 1.42857143;
    border-radius: 4px;
    color: #fff;
    background-color: #337ab7;
    border-color: #2e6da4;
    padding: 10px 16px;
    font-size: 18px;
    line-height: 1.3333333;
    border-radius: 6px;
}

a:focus {
    color: #fff;
    background-color: #286090;
    border-color: #122b40;
    text-decoration: none;
}

a:hover {
    color: #fff;
    background-color: #286090;
    border-color: #204d74;
    text-decoration: none;
}

a:active {
    color: #fff;
    background-color: #204d74;
    border-color: #122b40;
    text-decoration: none;
}

TODO: Clean up the extraneous styles in the code above. [Chasen can handle this.]

Verify

✏️ Update src/pages/Home/Home.test.tsx to be:

import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { beforeEach, describe, expect, it } from 'vitest';

import Home from './Home';

describe('Home component', () => {
  beforeEach(() => {
    render(
      <MemoryRouter>
        <Home />
      </MemoryRouter>
    );
  });

  it('renders the image with correct attributes', () => {
    const image = screen.getByAltText(/Restaurant table with glasses./i);
    expect(image).toBeInTheDocument();
    expect(image).toHaveAttribute('width', '250');
    expect(image).toHaveAttribute('height', '380');
  });

  it('renders the title', () => {
    const titleElement = screen.getByText(/Ordering food has never been easier/i);
    expect(titleElement).toBeInTheDocument();
  });

  it('renders the description paragraph', () => {
    const description = screen.getByText(/We make it easier/i);
    expect(description).toBeInTheDocument();
  });

  it('renders the link to the restaurants page', () => {
    const link = screen.getByRole('link', { name: /choose a restaurant/i });
    expect(link).toBeInTheDocument();
    expect(link).toHaveAttribute('href', '/restaurants');
  });
});

Exercise

  • Update the styles in Home.module.css to be usable as a CSS Module.
  • Update the <Home> component to include a styled link:
    • Use <Link> (from the previous section!) to create a link to the /restaurants page.
    • Import the styles from Home.module.css and apply them to the new link.

Solution

Click to see the solution

✏️ Update src/pages/Home/Home.module.css to be:

.chooseButton {
    display: inline-block;
    margin-bottom: 0;
    font-weight: normal;
    text-align: center;
    vertical-align: middle;
    touch-action: manipulation;
    cursor: pointer;
    background-image: none;
    border: 1px solid transparent;
    white-space: nowrap;
    padding: 6px 12px;
    font-size: 14px;
    line-height: 1.42857143;
    border-radius: 4px;
    color: #fff;
    background-color: #337ab7;
    border-color: #2e6da4;
    padding: 10px 16px;
    font-size: 18px;
    line-height: 1.3333333;
    border-radius: 6px;
}

.chooseButton:focus {
    color: #fff;
    background-color: #286090;
    border-color: #122b40;
    text-decoration: none;
}

.chooseButton:hover {
    color: #fff;
    background-color: #286090;
    border-color: #204d74;
    text-decoration: none;
}

.chooseButton:active {
    color: #fff;
    background-color: #204d74;
    border-color: #122b40;
    text-decoration: none;
}

✏️ Update src/pages/Home/Home.tsx to be:

import HeroImage from "place-my-order-assets/images/homepage-hero.jpg"
import { Link } from "react-router-dom"
import styles from "./Home.module.css"

const Home: React.FC = () => {
    return (
        <div className="homepage" style={{ margin: "auto" }}>
            <img
                alt="Restaurant table with glasses."
                height="380"
                src={HeroImage}
                width="250"
            />

            <h1>Ordering food has never been easier</h1>

            <p>
                We make it easier than ever to order gourmet food from your favorite
                local restaurants.
            </p>

            <p>
                <Link className={styles.chooseButton} to="/restaurants">
                    Choose a Restaurant
                </Link>
            </p>
        </div>
    )
}

export default Home

Having issues with your local setup? See the solution in StackBlitz or CodeSandbox.

Next steps

In the next section, we'll learn to manage state.