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.
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 tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^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.
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.
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!
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;
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.
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:
Now click on the Dark theme button, and you’ll see them switch!
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!
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.
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 nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
One Reply to "Theming React components with Tailwind CSS"
Thanks Brett! This helped me with a thing