Brett Fisher Full-stack web developer hailing from Colorado. Incoming Software Development Engineer at Amazon.

Theming React components with Tailwind CSS

8 min read 2280

Theming React Components with Tailwind CSS

Are you interested in creating your own themeable React component library? Maybe you’d like full control over your project’s color palette and want a wide array of different themes available to your users. Maybe you’re just a hobbyist trying to exercise your creative muscles. Whatever camp you’re in, look no further than this article!

We’ll be laying the foundation for creating a scalable, fully customizable, themeable React component library.

For our example project, we’ll be using Tailwind CSS to theme our components. If you haven’t used Tailwind CSS before, you’ve been missing out.

Tailwind is composed of a huge array of utility CSS classes. This means you don’t have to write any CSS — you just add the appropriate Tailwind class to your HTML elements to apply the desired styling. Margin, padding, background color, and just about everything else are one class away.

Combine this with reusable React components and you’re well on your way to creating a scalable, themeable library.

Set up the project

Let’s start by creating our React project:

$ yarn create react-app themed-react-project
$ cd themed-react-project

Next, we’ll set up Tailwind by following the instructions from the official docs. First, we’ll install Tailwind and its peer dependencies:

$ yarn add -D [email protected]:@tailwindcss/postcss7-compat [email protected]^7 [email protected]^9

Now we’ll need to install CRACO:

$ yarn add @craco/craco

In your package.json file, modify your start, build, and test scripts to use craco instead of react-scripts:

{
  /* ... */
  "scripts": {
      "start": "craco start",
      "build": "craco build",
      "test": "craco test",
      "eject": "react-scripts eject"
  },
  /* ... */
}

Notice that we are not using CRACO for the eject script.

We made a custom demo for .
No really. Click here to check it out.

Create a craco.config.js file at the project root with the following content:

// craco.config.js
module.exports = {
  style: {
    postcss: {
      plugins: [
        require('tailwindcss'),
        require('autoprefixer'),
      ],
    },
  },
}

Create a tailwind.config.js file at the project root that looks like this:

// tailwind.config.js
module.exports = {
  purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
  darkMode: false,
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
};

Finally, replace src/index.css with the following, which lets Tailwind use its styles at build time:

// src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;

We’re done setting everything up! Now let’s start working on our theme.

Theming with Tailwind

One of the best things about Tailwind is how configurable it is. Out of the box, it comes with a huge color palette. If you’re creating themeable components for your site, though, the default color palette may be a little too much.

Let’s just start out by defining three colors: primary, secondary, and a color for text. We can easily do this by changing our tailwind.config.js file to look like this:

// tailwind.config.js
module.exports = {
  purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
  darkMode: false, // or 'media' or 'class'
  theme: {
    colors: {
      primary: "blue",
      secondary: "red",
      "text-base": "white",
    },
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
};

Now we can use these colors for any Tailwind utility class that accepts a color. Let’s test out our theme by creating a custom button component.

Inside the src folder, create another folder called components. Now create a Button.js file inside that. We’ll use the colors we defined in our Tailwind config to define the background and text colors. We’ll also use some built-in Tailwind classes to round the corners and add some padding to the button:

// src/components/Button.js
const Button = ({ children, ...rest }) => {
  return (
    <button className="rounded-md bg-primary text-text-base px-3 py-1">
      {children}
    </button>
  );
};

export default Button;

Notice that we capture any extra props in the rest variable and then pass them to the base HTML button component. This is so we can define any typical button behavior like an onClick callback.

Let’s go to App.js and test out our button. We’ll remove the boilerplate code and just add the button we created:

// src/App.js
import Button from "./components/Button";
function App() {
  return (
    <div className="flex justify-center mt-5">
      <Button>Themed Button</Button>
    </div>
  );
}
export default App;

You should see our custom button with our primary color as the background!Blue Themed Button

This is a great first step, but our button isn’t very customizable since we hardcoded the color as bg-primary. What if we wanted to use our secondary color as the background? No problem — let’s just pass in the color as a prop and use string interpolation to dynamically define our button’s color:

// src/components/Button.js
const Button = ({ children, color, ...rest }) => {
  return (
    <button className={`rounded-md bg-${color} text-text-base px-3 py-1`} {...rest}>
      {children}
    </button>
  );
};

Button.defaultProps = {
  color: "primary",
};
export default Button;

We set the default color to primary so that we don’t have to pass in the prop every time. Let’s try changing the color to secondary back in App.js to make sure it works:

import Button from "./components/Button";
function App() {
  return (
    <div className="flex justify-center mt-5">
      <Button color="secondary">Themed Button</Button>
    </div>
  );
}
export default App;

Red Themed Button

This works great, but it would be nice to enforce the props we pass in. That way, if someone makes a typo with the color prop, there will be a warning message in the console explaining why the component isn’t behaving as expected.

Inside our components folder, let’s make a file called themeProps.js where we’ll define the props that will be common for all of our themed components:

// src/components/themeProps.js
import { oneOf } from "prop-types";
const props = {
  color: oneOf(["primary", "secondary"]),
};
export default props;

Now we can use themeProps in our custom Button component:

// src/components/Button.js
import themeProps from "./themeProps";
const Button = ({ children, color, ...rest }) => {
  return (
    <button className={`rounded-md bg-${color} text-text-base px-3 py-1`} {...rest}>
      {children}
    </button>
  );
};

Button.propTypes = {
  ...themeProps,
};

Button.defaultProps = {
  color: "primary",
};

export default Button;

With our prop types now enforced, let’s move on to dynamically defining our theme.

Dynamically defining the theme

Right now, we defined our primary, secondary, and text colors in our Tailwind configuration file.

We can define as many colors as we want in our Tailwind theme configuration, but we have one limitation: the colors we have picked are hardcoded into the configuration file. What if we’d like to dynamically switch out the theme at runtime? This would be useful if we had a dark mode, for example, and a user wanted to view our site on dark mode during the night.

Instead of hardcoding our color palette directly into our Tailwind configuration, we can instead define our colors using CSS variables. Then we can dynamically alter the value of those variables at runtime to change the theme as we please. Change your tailwind.config.js file to look like this:

// tailwind.config.js
module.exports = {
  purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
  darkMode: false, // or 'media' or 'class'
  theme: {
    colors: {
      primary: "var(--theme-primary)",
      secondary: "var(--theme-secondary)",
      "text-base": "var(--theme-text-base)",
    },
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
};

We’re using three variables called --theme-primary, --theme-secondary, and --theme-text-base in our Tailwind configuration file. They aren’t defined now, so let’s work on that. In the src folder of your React project, create a new folder called themes and add a file called base.js. This will be our base theme, and later we can add other themes as we please. Put this in your base.js file:

// src/themes/base.js
const baseTheme = {
 "--theme-primary": "blue",
 "--theme-secondary": "red",
 "--theme-text-base": "white"
};
export default baseTheme;

This maps the name of our CSS variables to the color we want to be associated with the variable, but the CSS variables themselves still won’t be defined. Luckily, it’s easy to set CSS variables in JavaScript, so let’s make a function that will accept a theme object and create corresponding CSS variables with the values we defined.

Create a utils.js file in your themes folder and add the following:

// src/themes/utils.js
export function applyTheme(theme) {
  const root = document.documentElement;
  Object.keys(theme).forEach((cssVar) => {
    root.style.setProperty(cssVar, theme[cssVar]);
  });
}

Now let’s use this function to apply our base theme when our app is mounted. We can do this with the useEffect hook. Modify your App.js file to look like this:

import { useEffect } from "react";
import Button from "./components/Button";
import { applyTheme } from "./themes/utils";
import baseTheme from "./themes/base";

function App() {
  useEffect(() => {
    applyTheme(baseTheme);
  }, []);
  return (
    <div className="flex justify-center mt-5">
      <Button color="secondary">Themed Button</Button>
    </div>
  );
}

export default App;

In the browser, the app should still look the way it did before. Right now we only have our base theme, and it would be nice to create a dark theme. However, before we do that, we can clean up the way we define our themes. Our base.js theme file directly maps CSS variable names to a color, but it would be nice to define our colors in a friendlier way.

Let’s create a createTheme function in our utils.js file that will map names like primary and secondary to their corresponding CSS variable names that we’ve decided to use. Here’s our updated utils.js file:

export function applyTheme(theme) {
  const root = document.documentElement;
  Object.keys(theme).forEach((cssVar) => {
    root.style.setProperty(cssVar, theme[cssVar]);
  });
}

export function createTheme({
  primary,
  secondary,
  textBase,
}) {
  return {
    "--theme-primary": primary,
    "--theme-secondary": secondary,
    "--theme-text-base": textBase,
  };
}

Now let’s tweak our base.js theme file to use the createTheme function:

import { createTheme } from "./utils";
const baseTheme = createTheme({
  primary: "blue",
  secondary: "red",
  textBase: "white",
});
export default baseTheme;

Let’s create a dark.js file in our themes folder that uses the same pattern to define a dark theme:

import { createTheme } from "./utils";
const darkTheme = createTheme({
  primary: "#212529",
  secondary: "#343A40",
  textBase: "white",
});
export default darkTheme;

Let’s modify App.js to show two buttons that can dynamically modify our theme:

import { useEffect } from "react";
import Button from "./components/Button";
import { applyTheme } from "./themes/utils";
import baseTheme from "./themes/base";
import darkTheme from "./themes/dark";

function App() {
  useEffect(() => {
    applyTheme(baseTheme);
  }, []);
  return (
    <div className="flex justify-center mt-5">
      <Button onClick={() => applyTheme(baseTheme)}>Base theme</Button>
      <Button color="secondary" onClick={() => applyTheme(darkTheme)}>
        Dark theme
      </Button>
    </div>
  );
}

export default App;

First you’ll see our base theme:

Light Mode Buttons

Now click on the Dark theme button, and you’ll see them switch!

Dark Mode Buttons

Adding hover

We could add as many themes as we’d like with as many colors as we want, but our buttons are still a little boring. They’re always the same color — we should make it so that they change to be a lighter color when the user hovers over them.

Luckily for us, Tailwind makes this super easy. All we have to do is add a hover: prefix to any class that we only want to be applied when the component is hovered over.

For both our dark theme and base theme, let’s define two more colors that will be applied when the button is hovered over: primary-light and secondary-light. Let’s first update our tailwind.config.js file to use these colors as defined by the CSS variables --theme-primary-light and --theme-primary-dark:

// tailwind.config.js
module.exports = {
  purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
  darkMode: false, // or 'media' or 'class'
  theme: {
    colors: {
      primary: "var(--theme-primary)",
      "primary-light": "var(--theme-primary-light)",
      secondary: "var(--theme-secondary)",
      "secondary-light": "var(--theme-secondary-light)",
      "text-base": "var(--theme-text-base)",
    },
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
};

Next, in our theme utils.js file, we’ll update our createTheme function to create these new variables:

// src/themes/utils.js
export function applyTheme(theme) {
  const root = document.documentElement;
  Object.keys(theme).forEach((cssVar) => {
    root.style.setProperty(cssVar, theme[cssVar]);
  });
}

export function createTheme({
  primary,
  primaryLight,
  secondary,
  secondaryLight, 
  textBase,
}) {
  return {
    "--theme-primary": primary,
    "--theme-primary-light": primaryLight,
    "--theme-secondary": secondary,
    "--theme-secondary-light": secondaryLight,
    "--theme-text-base": textBase,
  };
}

Now let’s define lighter variants of our primary and secondary colors in our base.js theme file:

// src/themes/base.js
import { createTheme } from "./utils";

const baseTheme = createTheme({
  primary: "blue",
  primaryLight: "#4d4dff",
  secondary: "red",
  secondaryLight: "#ff4d4d",
  textBase: "white",
});

export default baseTheme;

Now we’ll do the same for our dark theme:

// src/themes/dark.js
import { createTheme } from "./utils";

const darkTheme = createTheme({
  primary: "#212529",
  primaryLight: "#464f58",
  secondary: "#343A40",
  secondaryLight: "#737f8c",
  textBase: "white",
});

export default darkTheme;

Finally, we’ll apply the hover variants to our button. Remember, since color is passed in as a prop, we can use string interpolation to dynamically apply the correct hover variant.

All we need to do is add a class called hover:bg-${color}-light, and then that will become either hover:bg-primary-light or hover:bg-secondary-light depending on the prop:

// src/components/Button.js
import themeProps from "./themeProps";

const Button = ({ children, color, ...rest }) => {
  return (
    <button
      className={`rounded-md bg-${color} hover:bg-${color}-light text-text-base px-3 py-1`}
      {...rest}
    >
      {children}
    </button>
  );
};

Button.propTypes = {
  ...themeProps,
};

Button.defaultProps = {
  color: "primary",
};
export default Button;

Try it out in your browser, and you’ll notice your buttons change color when you hover over them!

Conclusion

We only made a themed button component, but we can easily create a huge array of themed components using the code we started with here.

Any themed library will also need a much larger color palette than the one we defined in this project. However, as we saw when adding our variants for primary and secondary colors, it’s easy to add more colors to our themes as needed.

For the full source code, you can access it all on my GitHub account.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Brett Fisher Full-stack web developer hailing from Colorado. Incoming Software Development Engineer at Amazon.

Leave a Reply