React: Styling

Happy July! I hope that wherever you are things are going well. We're in another, predictable heat wave here in Texas, and I've been keeping busy to try and stay cool. Earlier this month I went to Mexico City!

Joey on a patio in Mexico City surrounded by plants.

It was awesome, we ate so much good food, went to a few museums, and really just soaked in the cool weather. Also, talk about exercising skills in a (for me) non-native language. I really tried hard not to be a clueless tourist who didn't speak any Spanish. To prep for the trip I practiced Spanish for 3 months straight on Duolingo (and am still on there with a 110+ day streak 💅) and watched a telenovela pretty much daily to try and improve my listening comprehension. I definitely stumbled a lot on our trip, and had to fall back on "no entiendo," a few times, but by the end of our week there I was semi-competently able to order food at restaurants!

Anyway, life updates aside, I am continuing to dive into React, and today I have a big old post about how things get styled in React. Since it's a JavaScript framework that outputs HTML, it was never immediately clear to me as a beginner where CSS actually fit into the mix. And if you pay attention to things on Twitter X, then you'll see a lot of people arguing about best and worst practices, making it hard to dig through all of the noise. People have super strong opinions about styling in React. This post isn't about that. I don't want to get into things like performance trade offs. Instead, I simply want to discuss the four common ways that CSS makes its way into a React app, how they wind up rendering, and some high-level gotchas for each approach.

Global Styles

So let's start with global styles. Things like font styles, CSS custom properties, and maybe some basic CSS reset items are typically used on all pages of your site. These can be defined once and not have to be redefined for every page or component, a practice known as global styling.

The most common pattern for handling global styles is to define those styles within a standard CSS file, name it index.css or globals.css, and have it saved somewhere toward the top of the /src directory. This then gets imported into a top-level application file, like index.js or App.js.

For example, in a basic Create React App installation, there is a /src/index.css that looks something like this:

/* /src/index.css */
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

This file gets gets imported into /src/index.js like so:

// /src/index.js
import './index.css';

When you run this application, those styles get embedded within <style></style> tags within the <head></head> of the document:

<!-- Generated HTML -->
<html lang="en">
  <head>
    <!-- ...some other stuff like favicon, SEO meta -->
    <style>
      body {
        margin: 0;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
      }
      code {
        font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
      }
    </style>
  </head>
</html>

Next.js gives you a few more options when you install it, but a basic Next.js installation is very similar to Create React App. There will be a /src/styles/globals.css file:

/* /src/styles/globals.css */
:root {
  --max-width: 1100px;
  --border-radius: 12px;
  --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
    'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
    'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;

  --foreground-rgb: 0, 0, 0;
  --background-start-rgb: 214, 219, 220;
  --background-end-rgb: 255, 255, 255;
  /* ...some other CSS custom properties... */
}

* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}

html,
body {
  max-width: 100vw;
  overflow-x: hidden;
}

/* ...some other global CSS... */

a {
  color: inherit;
  text-decoration: none;
}

I have shortened the contents of that file for brevity here; the contents are not the point, the point is the overall strategy. Anyway, that file gets imported into /src/pages/_app.js:

// /src/pages/_app.js
import '@/styles/globals.css'

Note that they are doing some fancy import aliasing, but it's really no different than import styles from '../styles/globals.css. When you run the Next.js app, the contents of the CSS file also get embedded into <style></style> tags within the <head></head>:

<!-- Generated HTML -->
<html lang="en">
  <head>
    <!-- ...some other stuff like favicon, SEO meta -->
    <style>
      :root {
        --max-width: 1100px;
        --border-radius: 12px;
        --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
          'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
          'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;

        --foreground-rgb: 0, 0, 0;
        --background-start-rgb: 214, 219, 220;
        --background-end-rgb: 255, 255, 255;
        /* ...some other CSS custom properties... */
      }

      * {
        box-sizing: border-box;
        padding: 0;
        margin: 0;
      }

      html,
      body {
        max-width: 100vw;
        overflow-x: hidden;
      }

      /* ...some other global CSS... */

      a {
        color: inherit;
        text-decoration: none;
      }
    </style>
  </head>
</html>

Curiously, at the time of writing this post, Gatsby doesn't have a global CSS file. A basic installation yields Inline Styles on the two starter pages, /src/pages/404.js and /src/pages/index.js, which I will cover in the next section.

I want to pause here to say that there's absolutely nothing stopping you from using SASS/SCSS or writing your entire website's stylesheets within a global CSS file in this manner. However, if you go this route, you run the risk of loading unnecessary styles on pages where those CSS rulesets aren't used. You might also run into naming collisions or issues with scope or cascade. Of course, these aren't new problems, or problems that are unique to React. Systems like BEM sometimes help, but oftentimes also introduce another element of mental overhead.

Here is a simple card component composed in BEM:

Isn't that a thing of beauty? Shout out to Against Me! for the lyrics. I whipped it up super fast, so it's definitely very rough around the edges, but I wanted to have an easy page component that I can use as an example for the next few sections. The markup looks like this:

<div class="card">
  <h2 class="card__title">Not one more word tonight</h2>
  <p class="card__description">Between here and there, we'll put a distance the size of the ocean so now this heart can beat a skipping rhythm. As the cadence carries me, I almost drift away; far enough to forget that when it comes you cannot hesitate. And when found I will write an account and seal it in an envelope addressed to your last known residence.</p>
  <div class="card__actions">
    <a href="#" class="card__action card__action--primary">Sink</a>
    <a href="#" class="card__action card__action--secondary">Drown</a>
  </div>
</div>

And the CSS looks like this:

body {
  font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
  font-weight: 300;
  background-color: #38473e;
}

.card {
  max-width: 240px;
  border: 1px solid #778191;
  border-radius: 20px;
  margin: 20px;
  padding: 20px;
  background-color: #ffffff;
}

.card__title {
  font-size: 21px;
  font-weight: 700;
  line-height: 30px;
}

.card__description {
  margin-top: 16px;
  font-size: 18px;
  line-height: 27px;
}

.card__actions {
  margin-top: 32px;
  display: flex;
  flex-direction: row;
}

@media screen and (max-width: 759px) {
  .card__actions {
    flex-direction: column;
    margin-top: 0;
  }
}

.card__action {
  margin-right: 20px;
  border: 1px solid #778191;
  border-radius: 20px;
  padding: 20px;
  text-decoration: none;
  font-size: 18px;
  font-weight: 700;
  line-height: 27px;
  color: #ffffff;
}

.card__action--primary {
  border-color: #18522c;
  background-color: #18522c;
}

.card__action--primary:hover {
  border-color: #207d40;
  background-color: #207d40;
}

.card__action--secondary {
  border-style: #778191;
  background-color: #778191;
}

.card__action--secondary:hover {
  border-color: #283242;
  background-color: #283242;
}

Over the next sections of this blog post, I will recreate this component in React using Inline Styles, Styled Components, and CSS Modules. While these aren't the only ways to style things in the React ecosystem, they are the leading three approaches, and they let you tightly couple CSS styles with the pages, components, and elements that specifically use those styles.

Inline Styles

I mentioned previously that (depending on how you configure it when you bootstrap the app), a basic Gatsby starter app is styled using Inline Styles. I imagine they do this because it's a fairly un-opinionated way to add some basic styles that is easy enough to remove when you start creating your real app. But let's take a second to look at this approach and create the card component example above using it.

First I'll create a Card.js component:

// Card.js
function Card({ title, description, primaryAction, secondaryAction }) {
  return (
    <div>
      <h2>{title}</h2>
      <p>{description}</p>
      <div>
        <a href="#">{primaryAction}</a>
        <a href="#">{secondaryAction}</a>
      </div>
    </div>
  );
}

export default Card;

I'll import this into <App /> or wherever else within my app that I need to. Check out the Components section of my Structure, Components, and Fragments post to see how a component gets imported throughout a React app. I'll call this component within <App /> like so:

// App.js
<Card
  title="Not one more word tonight"
  description="Between here and there, we'll put a distance the size of the ocean so now this heart can beat a skipping rhythm. As the cadence carries me, I almost drift away; far enough to forget that when it comes you cannot hesitate. And when found I will write an account and seal it in an envelope addressed to your last known residence."
  primaryAction="Sink"
  secondaryAction="Drown"
/>

When you apply Inline Styles in regular HTML, it looks something like the following:

<div style="max-width: 240px; border: 1px solid #778191; border-radius: 20px; margin: 20px; padding: 20px; background-color: #ffffff;">
  ...
</div>

In React, these same Inline Styles are applied like this:

// Card.js
function Card({ title, description, primaryAction, secondaryAction }) {
  return (
    <div
      style={{
        maxWidth: 240,
        border: "1px solid #778191",
        borderRadius: 20,
        margin: 20,
        padding: 20,
        backgroundColor: "#ffffff",
      }}
    >
      <h2>{title}</h2>
      <p>{description}</p>
      <div>
        <a href="#">{primaryAction}</a>
        <a href="#">{secondaryAction}</a>
      </div>
    </div>
  );
}

export default Card;

There are a few grammatical adjustments needed to make this happen. To begin with, the style attribute accepts a JavaScript object within the expression slot (hence the double {{}} curly brackets). Because it's an object, the style property names are written in camelCase, rather than the usual CSS string (i.e., borderRadius instead of border-radius). It's a little strange, but it is consistent with the DOM style JavaScript properties.

You'll also notice that declarations are demarcated by a comma , instead of a semi-colon ; as they would be in regular CSS:

maxWidth: 240,
borderRadius: 20,

vs.

max-width: 240px;
border-radius: 20px;

Another gotcha here comes with units. You'll notice that where before we used 240px and 20px for max-width and border-radius, respectively, here we simply use 240 and 20. That's because when we simply pass a numeric value within a style object, React automatically appends the px suffix to that value. If you want to use an alternative unit, such as rem or em, you need to specify that entire value as a string:

maxWidth: '1.5rem'

It's worth calling out that there are a good number of CSS properties that accept unitless numbers that aren't measured in or converted to "px". Thise can be found here.

Compound or shorthand CSS values will also need to be registered as strings. The border shorthand value is a good example of this:

border: "1px solid #778191",

Proceeding in this manner gets us most of the way there...

// Card.js
function Card({ title, description, primaryAction, secondaryAction }) {
  return (
    <div
      style={{
        maxWidth: 240,
        border: "1px solid #778191",
        borderRadius: 20,
        margin: 20,
        padding: 20,
        backgroundColor: "#ffffff",
      }}
    >
      <h2
        style={{
          fontSize: 21,
          fontWeight: 700,
          lineHeight: "30px",
        }}
      >
        {title}
      </h2>
      <p
        style={{
          marginTop: 16,
          fontSize: 18,
          lineHeight: "27px",
        }}
      >
        {description}
      </p>
      <div>
        <a href="#">{primaryAction}</a>
        <a href="#">{secondaryAction}</a>
      </div>
    </div>
  );
}

export default Card;

But it's admittedly pretty gross to read through. This manner really dilutes the markup with all of those styles. A common approach here is to move those styles into their own declarations and then pass them to the style prop like so:

// Card.js
function Card({ title, description, primaryAction, secondaryAction }) {
  const cardStyle = {
    maxWidth: 240,
    border: "1px solid #778191",
    borderRadius: 20,
    margin: 20,
    padding: 20,
    backgroundColor: "#ffffff",
  };

  const titleStyle = {
    fontSize: 21,
    fontWeight: 700,
    lineHeight: "30px",
  };

  const descriptionStyle = {
    marginTop: 16,
    fontSize: 18,
    lineHeight: "27px",
  };

  return (
    <div style={cardStyle}>
      <h2 style={titleStyle}>{title}</h2>
      <p style={descriptionStyle}>{description}</p>
      <div>
        <a href="#">{primaryAction}</a>
        <a href="#">{secondaryAction}</a>
      </div>
    </div>
  );
}

export default Card;

Now that's a bit better.

It is worth pausing for a moment here to observe how these styles actually render into the DOM. Let's look at this portion of the above code:

// Card.js
<div style={cardStyle}>
  <h2 style={titleStyle}>{title}</h2>
</div>

If we inspect this in the browser, this is how it renders:

<!-- Generated HTML -->
<div style="max-width: 240px; border: 1px solid rgb(119, 129, 145); border-radius: 20px; margin: 20px; padding: 20px; background-color: rgb(255, 255, 255);">
  <h2 style="font-size: 21px; font-weight: 700; line-height: 30px;">Not one more word tonight</h2>
</div>

It truly transpiles to inline CSS.

We still need to handle our action links though. These are a little more complicated since they share certain styles, but other styles differ depending on whether it's a primary or secondary action. Let's try to go about this in a reusable React manner.

So far I've rendered this component like so:

// App.js
<Card
  title="Not one more word tonight"
  description="Between here and there, we'll put a distance the size of the ocean so now this heart can beat a skipping rhythm. As the cadence carries me, I almost drift away; far enough to forget that when it comes you cannot hesitate. And when found I will write an account and seal it in an envelope addressed to your last known residence."
  primaryAction="Sink"
  secondaryAction="Drown"
/>

I'll adjust this by removing primaryAction and secondaryAction altogether and bundling both of these up into an actions array prop:

// App.js
<Card
  title="Not one more word tonight"
  description="Between here and there, we'll put a distance the size of the ocean so now this heart can beat a skipping rhythm. As the cadence carries me, I almost drift away; far enough to forget that when it comes you cannot hesitate. And when found I will write an account and seal it in an envelope addressed to your last known residence."
  actions={[
    {
      label: "Sink",
    },
    {
      label: "Drown",
    },
  ]}
/>

Now we need to adjust this portion of the component:

// Card.js
<div>
  <a href="#">{primaryAction}</a>
  <a href="#">{secondaryAction}</a>
</div>

And make it into this:

// Card.js
<div>
  {actions.map((action, i) => (
    <a href="#" key={i}>
      {action.label}
    </a>
  ))}
</div>

We also need to adjust the destructuring at the top of the component function call:

// Card.js
function Card({ title, description, actions }) {
  // ...
}

I want to add some styling to the container <div></div> for the actions:

// Card.js
function Card({ title, description, actions }) {
  // ... other styles

  const actionsStyle = {
    marginTop: 32,
    display: "flex",
    flexDirection: "row",
  };

  return (
    <div style={cardStyle}>
      <h2 style={titleStyle}>{title}</h2>
      <p style={descriptionStyle}>{description}</p>
      <div style={actionsStyle}>
        {actions.map((action, i) => (
          <a href="#" key={i} style={actionStyle}>
            {action.label}
          </a>
        ))}
      </div>
    </div>
  );
}

With this in place, I will further break out the action into its own component:

// Action.js
function Action({ action }) {
  const { label, primary } = action;

  return (
    <a href="#" style={actionStyle}>
      {label}
    </a>
  );
}
export default Action;

And import it like so:

// Card.js
import Action from "./Action";

function Card({ title, description, actions }) {
  import Action from "./Action";

  // ... other styles

  const actionsStyle = {
    marginTop: 32,
    display: "flex",
    flexDirection: "row",
  };

  return (
    <div style={cardStyle}>
      <h2 style={titleStyle}>{title}</h2>
      <p style={descriptionStyle}>{description}</p>
      <div style={actionsStyle}>
        {actions.map((action, i) => (
          <Action action={action} key={i} />
        ))}
      </div>
    </div>
  );
}

Now, let's specify whether an action is primary or not:

// App.js
<Card
  title="Not one more word tonight"
  description="Between here and there, we'll put a distance the size of the ocean so now this heart can beat a skipping rhythm. As the cadence carries me, I almost drift away; far enough to forget that when it comes you cannot hesitate. And when found I will write an account and seal it in an envelope addressed to your last known residence."
  actions={[
    {
      label: "Sink",
      primary: true,
    },
    {
      label: "Drown",
      primary: false,
    },
  ]}
/>

We're going to aim to use the primary boolean to render styles conditionally based on whether that boolean value is true or false. But first, let's take care of the styles that both action links will share, regardless of whether they are primary or not:

// Action.js
function Action({ action }) {
  const { label, primary } = action;

  const actionStyle = {
    marginRight: 20,
    border: "1px solid #778191",
    borderRadius: 20,
    padding: 20,
    textDecoration: "none",
    fontSize: 18,
    fontWeight: 700,
    lineHeight: "27px",
    color: "#ffffff",
  };

  return (
    <a href="#" style={actionStyle}>
      {label}
    </a>
  );
}
export default Action;

And now, within the Action component, we can use the primary prop to conditionally set styles:

// Action.js
function Action({ action }) {
  const { label, primary } = action;

  const actionStyle = {
    marginRight: 20,
    border: "1px solid #778191",
    borderRadius: 20,
    padding: 20,
    textDecoration: "none",
    fontSize: 18,
    fontWeight: 700,
    lineHeight: "27px",
    color: "#ffffff",
    borderColor: primary ? "#18522c" : "#778191",
    backgroundColor: primary ? "#18522c" : "#778191",
  };

  return (
    <a href="#" style={actionStyle}>
      {label}
    </a>
  );
}
export default Action;

As complicated as this is, you'll notice that I still haven't accounted for the media query or the hover styles. As with regular Inline Styles in HTML, Inline Styles in React cannot on their own work with media queries or pseudo classes. To get :hover styles working, we would probably need to use the useState() Hook, and to get the media query working we might have to write our own custom hook. This is wildly complicated.

And it's an antipattern. The React docs themselves discourage using Inline Styles. I've gone down this rabbit hole for the sake of demonstrating how complicated this method is, from the camelCasing to the odd string interpolation to keeping track of which units you need to explicitly include to the limitations for basic CSS features - better ways exist, like CSS Modules and Styled Components. Let's look into those approaches now.

CSS Modules

CSS Modules is a styling approach that comes out of the box with (I believe) all of the major, modern React frameworks. Here's how it works. I'm going to take the CSS styles for our Card component and put them into a file named Card.module.css. This naming convention tells React that this file is available to be imported and consumed as a JavaScript object.

/* Card.module.css */
.card {
  max-width: 240px;
  border: 1px solid #778191;
  border-radius: 20px;
  margin: 20px;
  padding: 20px;
  background-color: #ffffff;
}

.title {
  font-size: 21px;
  font-weight: 700;
  line-height: 30px;
}

.description {
  margin-top: 16px;
  font-size: 18px;
  line-height: 27px;
}

.actions {
  margin-top: 32px;
  display: flex;
  flex-direction: row;
}

@media screen and (max-width: 759px) {
  .actions {
    flex-direction: column;
    margin-top: 0;
  }
}

You'll note that this is just regular, plain CSS - no weird camelCasing or string interpolation adjustments. You'll also note that I'm not using the BEM structure here. I can simply use class names such as title or description instead of card__title or card__description. More on this in a little bit.

Back in my Card.js component file, I need to import the styles and actually apply them:

// Card.js
import Action from "./Action";
import styles from "./Card.module.css";

function Card({ title, description, actions }) {
  return (
    <div className={styles.card}>
      <h2 className={styles.title}>{title}</h2>
      <p className={styles.description}>{description}</p>
      <div className={styles.actions}>
        {actions.map((action, i) => (
          <Action action={action} key={i} />
        ))}
      </div>
    </div>
  );
}

export default Card;

At the top of App.js, it gets imported like any other JavaScript module into the styles namespace. Then we can apply the styles we defined by calling their class names as properties on that styles object, like <h2 className={styles.title}>{title}</h2>.

What's neat about CSS Modules is that it scopes these styles directly to the component and element where they are imported and used. Let's take a look again at this portion of the above code:

// Card.js
<div className={styles.card}>
  <h2 className={styles.title}>{title}</h2>
</div>

If we inspect this in the browser, we'll see something like the following:

<!-- Generated HTML -->
<div class="Card_card__7Eml9">
  <h2 class="Card_title__qr0vL">Not one more word tonight</h2>
</div>

React automatically transpiles the generic class names we declared in the CSS file into unique class names scoped to that instance of that component. This way we don't have to worry about scoping or cascade issues. The styles also automatically get those names too:

/* Generated CSS */
.Card_card__7Eml9 {
  max-width: 240px;
  border: 1px solid #778191;
  border-radius: 20px;
  margin: 20px;
  padding: 20px;
  background-color: #ffffff;
}

.Card_title__qr0vL {
  font-size: 21px;
  font-weight: 700;
  line-height: 30px;
}

You'll notice too that because Card.module.css is just a CSS file, we can also include media queries and it works just fine (unlike with Inline Styles).

Now let's move onto the Action component. To start, I'll break out the styles into Action.module.css:

/* Action.module.css */
.action {
  margin-right: 20px;
  border: 1px solid #778191;
  border-radius: 20px;
  padding: 20px;
  text-decoration: none;
  font-size: 18px;
  font-weight: 700;
  line-height: 27px;
  color: #ffffff;
}

.primary {
  border-color: #18522c;
  background-color: #18522c;
}

.primary:hover {
  border-color: #207d40;
  background-color: #207d40;
}

.secondary {
  border-color: #778191;
  background-color: #778191;
}

.secondary:hover {
  border-color: #283242;
  background-color: #283242;
}

You'll see we have our main action class with the styles shared between the two buttons, and then primary and secondary classes that will be applied conditionally based on the primary prop value. Let's accommodate all of this in Action.js.

// Action.js
import styles from "./Action.module.css";

function Action({ action }) {
  const { label, primary } = action;

  return (
    <a
      href="#"
      className={`${styles.action} ${
        primary ? styles.primary : styles.secondary
      }`}
    >
      {label}
    </a>
  );
}
export default Action;

As before, we import the Action.module.css module into the styles namespace. Then for the className, we use a template literal and a ternary operator to concatenate styles.action (for the shared styles) with either styles.primary or styles.secondary, depending on whether the primary prop value is true or false. Pretty neat!

This renders as something like the following:

<!-- Generated HTML -->
<a href="#" class="Action_action__86RW8 Action_primary__MXZLV">Sink</a>
<a href="#" class="Action_action__86RW8 Action_secondary__oftzz">Drown</a>
/* Generated CSS */
.Action_action__86RW8 {
  margin-right: 20px;
  border: 1px solid #778191;
  border-radius: 20px;
  padding: 20px;
  text-decoration: none;
  font-size: 18px;
  font-weight: 700;
  line-height: 27px;
  color: #ffffff;
}

.Action_primary__MXZLV {
  border-color: #18522c;
  background-color: #18522c;
}

.Action_secondary__oftzz {
  border-color: #778191;
  background-color: #778191;
}

If you need to do even more sophisticated work with conditional class names, you can look into something like the Classnames utility library.

There's still a little bit of overhead remembering how to properly export and import the CSS files, and I believe there is some weirdness around using selectors other than CSS classes in your CSS files, but CSS Modules is an interesting way of solving some of the scoping issues while maintaining something of a traditional external stylesheet structure. You can even add in SASS if you need to!

Styled Components

The last approach I will cover here is Styled Components. Unlike Inline Styles or CSS Modules, Styled Components is typically not supported out of the box by the major React frameworks and typically needs to be manually installed (or selected when you create your app through the CLI, as with Gatsby). The docs for this package can be found here.

To get started, I'll add styled-components to my project using yarn add styled-components. For NPM pals, that would be npm install --save styled-components.

Styled Components works by letting you use tagged template literals to define the elements you want to style as components. That's a lot of big words, so let's take this step by step.

The first thing you need to do is import styled-components to the styled namespace at the top of the Component file where you want to use it. I'll start in the Card.js file:

// Card.js
import Action from "./Action";
import styled from "styled-components";

function Card({ title, description, actions }) {
  return (
    <div>
      <h2>{title}</h2>
      <p>{description}</p>
      <div>
        {actions.map((action, i) => (
          <Action action={action} key={i} />
        ))}
      </div>
    </div>
  );
}

export default Card;

Now we need to choose an element we want to style. I'll go for the wrapper <div></div> first. To create it as a styled component, you need to do the following:

// Card.js
const CardWrapper = styled.div``;

Now you simply need to put your CSS within the `` backticks. One thing I'll call out here is that I went with CardWrapper here because if I simply called it Card, I would have a naming conflict with the Card component itself.

// Card.js
const CardWrapper = styled.div`
  max-width: 240px;
  border: 1px solid #778191;
  border-radius: 20px;
  margin: 20px;
  padding: 20px;
  background-color: #ffffff;
`;

With your styles in place, you simply use CardWrapper instead of the <div></div>:

// Card.js
import Action from "./Action";
import styled from "styled-components";

const CardWrapper = styled.div`
  max-width: 240px;
  border: 1px solid #778191;
  border-radius: 20px;
  margin: 20px;
  padding: 20px;
  background-color: #ffffff;
`;

function Card({ title, description, actions }) {
  return (
    <CardWrapper>
      <h2>{title}</h2>
      <p>{description}</p>
      <div>
        {actions.map((action, i) => (
          <Action action={action} key={i} />
        ))}
      </div>
    </CardWrapper>
  );
}

export default Card;

It's as easy as that. For a further example, here's the <h2></h2> element as a styled component:

// Card.js
import Action from "./Action";
import styled from "styled-components";

const CardWrapper = styled.div`
  max-width: 240px;
  border: 1px solid #778191;
  border-radius: 20px;
  margin: 20px;
  padding: 20px;
  background-color: #ffffff;
`;

const Title = styled.h2`
  font-size: 21px;
  font-weight: 700;
  line-height: 30px;
`;

function Card({ title, description, actions }) {
  return (
    <CardWrapper>
      <Title>{title}</Title>
      <p>{description}</p>
      <div>
        {actions.map((action, i) => (
          <Action action={action} key={i} />
        ))}
      </div>
    </CardWrapper>
  );
}

export default Card;

It's once again worth taking some time to see how this renders onto the page. Let's take a look at these two elements.

<!-- Generated HTML -->
<div class="sc-gsnScm iZThhW">
  <h2 class="sc-dkzCjb ewkYmk">Not one more word tonight</h2>
</div>

You'll see that, similar to CSS Modules, Styled Components takes care of generating unique class names. This helps with any naming conflicts and scoping issues. Styled Components also injects your defined styles into the <head> of the document:

<!-- Generated HTML -->
<head>
  <style data-styled="active" data-styled-version="6.0.5">.iZThhW{max-width:240px;border:1px solid #778191;border-radius:20px;margin:20px;padding:20px;background-color:#ffffff;}.ewkYmk{font-size:21px;font-weight:700;line-height:30px;}</style>
</head>

Just to round things out, here is the rest of the Card component, styled with Styled Components:

// Card.js
import Action from "./Action";
import styled from "styled-components";

const CardWrapper = styled.div`
  max-width: 240px;
  border: 1px solid #778191;
  border-radius: 20px;
  margin: 20px;
  padding: 20px;
  background-color: #ffffff;
`;

const Title = styled.h2`
  font-size: 21px;
  font-weight: 700;
  line-height: 30px;
`;

const Description = styled.p`
  margin-top: 16px;
  font-size: 18px;
  line-height: 27px;
`;

const Actions = styled.div`
  margin-top: 32px;
  display: flex;
  flex-direction: row;
`;

function Card({ title, description, actions }) {
  return (
    <CardWrapper>
      <Title>{title}</Title>
      <Description>{description}</Description>
      <Actions>
        {actions.map((action, i) => (
          <Action action={action} key={i} />
        ))}
      </Actions>
    </CardWrapper>
  );
}

export default Card;

One last thing to account for on the Card component is the media query that changes the flex-direction of the action. Styled Components makes this very easy by letting you embed media queries directly within the ruleset, similar to SASS/SCSS:

// Card.js
import Action from "./Action";
import styled from "styled-components";

const CardWrapper = styled.div`
  max-width: 240px;
  border: 1px solid #778191;
  border-radius: 20px;
  margin: 20px;
  padding: 20px;
  background-color: #ffffff;
`;

const Title = styled.h2`
  font-size: 21px;
  font-weight: 700;
  line-height: 30px;
`;

const Description = styled.p`
  margin-top: 16px;
  font-size: 18px;
  line-height: 27px;
`;

const Actions = styled.div`
  margin-top: 32px;
  display: flex;
  flex-direction: row;
  @media screen and (max-width: 759px) {
    flex-direction: column;
    margin-top: 0;
  }
`;

function Card({ title, description, actions }) {
  return (
    <CardWrapper>
      <Title>{title}</Title>
      <Description>{description}</Description>
      <Actions>
        {actions.map((action, i) => (
          <Action action={action} key={i} />
        ))}
      </Actions>
    </CardWrapper>
  );
}

export default Card;

Now I want to move onto the Action component. Handling the shared styles is pretty straightforward:

// Action.js
import styled from "styled-components";

const ActionLink = styled.a`
  margin-right: 20px;
  border: 1px solid #778191;
  border-radius: 20px;
  padding: 20px;
  text-decoration: none;
  font-size: 18px;
  font-weight: 700;
  line-height: 27px;
  color: #ffffff;
`;

function Action({ action }) {
  const { label, primary } = action;

  return (
    <ActionLink href="#">
      {label}
    </ActionLink>
  );
}
export default Action;

But how do I handle the dynamic styles - the background and border colors that are different based on whether the primary prop is true or false? Luckily, Styled Components makes that a breeze. Since your styled component is itself a React component, you can simply pass it a prop:

// Action.js
<ActionLink href="#" primary={primary}>
  {label}
</ActionLink>

And since the styles are defined within a template literal, you can use that prop as a variable within an expression statement to return the styles you want:

// Action.js
border-color: ${(props) => (props.primary ? "#18522c" : "#778191")};
background-color: ${(props) => (props.primary ? "#18522c" : "#778191")};

Very cool. The last thing that we need to take care of here is the hover state. Once again, Styled Components lets us define pseudoclass styles directly in the ruleset similar to SASS/SCSS:

// Action.js
import styled from "styled-components";

const ActionLink = styled.a`
  margin-right: 20px;
  border: 1px solid #778191;
  border-radius: 20px;
  padding: 20px;
  text-decoration: none;
  font-size: 18px;
  font-weight: 700;
  line-height: 27px;
  color: #ffffff;
  border-color: ${(props) => (props.primary ? "#18522c" : "#778191")};
  background-color: ${(props) => (props.primary ? "#18522c" : "#778191")};
  &:hover {
    border-color: ${(props) => (props.primary ? "#207d40" : "#283242")};
    background-color: ${(props) => (props.primary ? "#207d40" : "#283242")};
  }
`;

function Action({ action }) {
  const { label, primary } = action;

  return (
    <ActionLink href="#" primary={primary}>
      {label}
    </ActionLink>
  );
}
export default Action;

Summary

  • Global styles will often be found in a CSS file that gets imported into your index.js or App.js file, and those styles get embedded into the <head></head> of each page.
  • Inline Styles and CSS Modules work out of the box with all of the major React libraries/frameworks. Styled Components does not and needs to be added manually.
  • Inline Styles translate into old fashioned inline CSS within your markup. There are grammatical changes that need to be made to work with Inline Styles in React. As with traditional inline CSS, things like pseudoclasses and media queries do not work. If you need them you would need to use useState() or custom hooks
  • CSS Modules let you write the CSS for a specific React component in a CSS file that is scoped to that component. Class selectors are automatically transpiled to names that are unique to that component. This helps fix issues with scoping, naming collisions, and other cascade issues. The CSS gets generated as separate CSS files that get loaded in the browser.
  • Styled Components let you write the CSS for a specific React component in that component's JavaScript file. You target elements as components and apply styles using string tagged template literals. As with other React components, you can pass props to these styled components to easily create dynamic styles. The CSS generated gets embedded into the <head></head> of the pages where that component gets used.

Sources / Further Reading