An unopinionated, scalable, tailwindcss theming solution
🎨 Theme anything: Anything that extends tailwind's config and can be expressed in css variables is something you can theme with this plugin, even other plugins
🍨 Unlimited themes: You can have as many themes as you want! This plugin doesn't care!
💫 Automatic variants: Automatically generate variants for all of your themes (i.e. use classes like my-theme:font-black
) to enable classes only when certain themes active.
🌑 Trivial dark theme: Because dark theme is just another theme implementing dark theme is as easy as naming the theme you create as "dark" (or whatever you want), no special config
🤖 Automatically handles colors and opacity: Using tailwind with css variables can get tricky with colors, but this plugin handles all of that for you!
😅 Easy theme management: A simple, declarative api that lets you easily create and modify themes
👋 Familiar api: The way you declare themes is the exact same way you extend tailwind with the exact same features
💻 Modern: Powered by css variables under the hood
🚀 Designed to reduce bundle size: Instead of duplicating all of your style definitions, the use of css variables lets you declare styles once
- Examples
- Getting Started
- How it works
- Documentation
- Enabling your theme
- Typescript
- Common problems
- Want to suggest additional features?
- Didn't find what you were looking for?
Install tailwindcss-themer
using yarn
:
yarn add --dev tailwindcss-themer
Or npm
:
npm install --save-dev tailwindcss-themer
In your tailwind.config.js
file, add tailwindcss-themer
to the plugins
array
// tailwind.config.js
module.exports = {
theme: {
// ...
},
plugins: [
require('tailwindcss-themer')
// ...
]
}
Pass the plugin a config object representing your theme configuration (see Config for details)
// tailwind.config.js
module.exports = {
theme: {
// ...
},
plugins: [
require('tailwindcss-themer')({
defaultTheme: {
// put the default values of any config you want themed
// just as if you were to extend tailwind's theme like normal https://tailwindcss.com/docs/theme#extending-the-default-theme
extend: {
// colors is used here for demonstration purposes
colors: {
primary: 'red'
}
}
},
themes: [
{
// name your theme anything that could be a valid css selector
// remember what you named your theme because you will use it as a class to enable the theme
name: 'my-theme',
// put any overrides your theme has here
// just as if you were to extend tailwind's theme like normal https://tailwindcss.com/docs/theme#extending-the-default-theme
extend: {
colors: {
primary: 'blue'
}
}
}
]
})
// ...
]
}
<!-- this example uses pure html for demonstration purposes -->
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
</head>
<body>
<!-- this has "color: 'red'" assigned to it because that's what was specified as the default-->
<h1 class="text-primary">Hello world!</h1>
</body>
</html>
You do this by adding a class of the theme's name to whatever you want themed. See Enabling your theme for more details.
<!-- this example uses pure html for demonstration purposes -->
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
</head>
<!-- everything within this tag now has the "my-theme" config applied to it -->
<body class="my-theme">
<!-- this has "color: 'blue'" assigned to it because that's what was specified in the "my-theme" config -->
<h1 class="text-primary">Hello world!</h1>
</body>
</html>
This plugin doesn't care how you apply the class. That's up to you. All this plugin cares about is that the class is applied.
If for some reason you need to apply classes only when certain themes are active and you can't express what you want via the normal theming process shown, you can use the automatically generated variants for each of your themes!
<!-- this class will only activate when the "my-theme" class is active -->
<h1 class="my-theme:font-bold">Hello world!</h1>
See Variants for more details.
This plugin works by first creating a custom tailwind extension configured to replace any values you specify with css variables. Then it generates css variables with proper scoping for all of your themes. It also creates variants for each one of your themes as a bonus.
In short it automates everything you would need to do to do this yourself plus adds theme variants for you.
require('tailwindcss-themer')({
defaultTheme: {
extend: {
colors: {
primary: 'red'
},
fontFamily: {
title: 'Helvetica'
}
}
},
themes: [
{
name: 'my-theme',
extend: {
colors: {
primary: 'blue'
},
fontFamily: {
title: 'ui-monospace'
}
}
}
]
})
For example, the above configuration creates a theme extension equivalent to the following hand written version.
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: ({ opacityVariable, opacityValue }) => {
if (opacityValue !== undefined) {
return `rgba(var(--colors-primary), ${opacityValue})`
}
if (opacityVariable !== undefined) {
return `rgba(var(--colors-primary), var(${opacityVariable}, 1))`
}
return `rgb(var(--colors-primary))`
}
},
fontFamily: {
title: 'var(--font-family-title)'
}
}
}
}
Notice how we needed to set
color.primary
to a callback function. This is to properly handle opacity. See Opacity for more details.
Because it creates a theme extension for you, this is why it overwrites whatever is in the normal theme extension upon collision. See This plugin's config overwrites what is in the normal tailwind config n collision for more details.
It also injects css variables with proper scoping into tailwind's base layer.
/* this is configured by "defaultTheme" */
:root {
--colors-primary: 255, 0, 0;
--font-family-title: Helvetica;
}
/* this is configured by the "my-theme" configuration */
.my-theme {
--colors-primary: 0, 0, 255;
--font-family-title: ui-monospace;
}
Notice how the css variable for the color we specified is broken up into rgb values. We need to do this to support opacity modifiers. See Opacity for more details.
Now, classes like text-primary
and font-title
are generated like the following:
.font-title {
font-family: var(--font-family-title);
}
.text-primary {
--tw-text-opacity: 1;
color: rgba(var(--colors-primary), var(--tw-text-opacity));
}
For comparison, this is what those classes looked like before without theming:
.font-title {
font-family: Helvetica;
}
.text-primary {
--tw-text-opacity: 1;
color: rgb(255 0 0 1 / var(--tw-text-opacity));
}
This plugin adds variants for each one of your themes, should you need them when applying classes.
Taking, again, the above configuration as an example, the my-theme
variant is generated for my-theme
. So now you can use classes like my-theme:font-title
which will enable the classes only when the theme is enabled. The generated css for this example is the following:
.my-theme .my-theme\:font-title {
font-family: var(--font-family-title);
}
All css variables are injected in tailwind's base layer.
All of the configuration in the defaultTheme
config generates css variables scoped to :root
.
require('tailwindcss-themer')({
defaultTheme: {
extend: {
colors: {
primary: 'red'
},
fontFamily: {
title: 'Helvetica'
}
}
}
// ...
})
For example the above defaultTheme
config generates the following css variables in the :root
scope.
/* this is configured by "defaultTheme" */
:root {
--colors-primary: 255, 0, 0;
--font-family-title: Helvetica;
}
For each theme specified in the themes
section of the config, its config generates css variables scoped to a class of the name
field.
require('tailwindcss-themer')({
// ...
themes: [
{
name: 'my-theme-1',
extend: {
colors: {
primary: 'red'
},
fontFamily: {
title: 'Helvetica'
}
}
},
{
name: 'my-theme-2',
extend: {
colors: {
primary: 'blue'
},
fontFamily: {
title: 'ui-monospace'
}
}
}
]
})
For example, the above config in the themes
section of the config generates the following css:
.my-theme-1 {
--colors-primary: 255, 0, 0;
--font-family-title: Helvetica;
}
.my-theme-2 {
--colors-primary: 0, 0, 255;
--font-family-title: ui-monospace;
}
As specified above, variants are generated for every named theme you make, even for the default theme. This is so you can use them as class modifiers to enable certain styles only when that theme is enabled. It works like hover and focus variants, but activated with the theme. This lets you write classes like my-theme:rounded-sm
if you need fine grained control to apply some styles when a theme is activated and you can't cleanly express what you want with css variables alone.
Do note that because tailwind automatically adds the dark
variant, if you name one of your themes dark
, the variant this plugin creates for it will conflict with what tailwind automatically creates for you. It is recommended that you name your dark theme something else like darkTheme
to avoid the conflict or you could set darkMode: 'class' in your tailwind.config.js
The theme variant generated for the default theme is defaultTheme
(e.g. defaultTheme:rounded-sm
), but this now requires that instead of omitting any theme class to enable the default theme, you explicitly declare you are using the default theme by adding the class of defaultTheme
to the place you want themed (no other feature is affected by this, using the default theme variant is the only feature that requires you to add the defaultTheme
class to use). This is because I haven't been able to create a css selector that excludes all parents with any of the other theme classes. If you can make one, feel free to open up an issue.
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
</head>
<body>
<!-- this won't work because it doesn't have a parent with the "defaultTheme" class -->
<!-- I would love to make this work, but I haven't come up with a css selector that would work -->
<h1 class="defaultTheme:font-bold">Hello world!</h1>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
</head>
<body class="defaultTheme">
<!-- this now works -->
<h1 class="defaultTheme:font-bold">Hello world!</h1>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
</head>
<body class="neon">
<!-- this is turned off because it doesnt have a parent with a class of "defaultTheme" -->
<h1 class="defaultTheme:font-bold">Hello world!</h1>
</body>
</html>
As you probably could tell from above, the names of the generated css variables are the kebab-cased version of the variable's path on the config object.
Naming works the same for all theme configs (default theme and named themes).
require('tailwindcss-themer')({
defaultTheme: {
extend: {
colors: {
myBrand: {
primary: {
500: 'red'
}
}
}
}
}
// ...
})
The above config would generate a css variable of the name --colors-myBrand-primary-500
. If for some reason, camelCasing is converted to kebab-casing, make sure you have tailwind v3.0.12
or later installed as that version fixed that bug.
If you use DEFAULT
anywhere on a path to a variable, it is dropped off of the generated css variable name.
require('tailwindcss-themer')({
defaultTheme: {
extend: {
colors: {
brand1: {
DEFAULT: {
primary: {
DEFAULT: 'red'
}
}
}
}
}
}
// ...
})
The above config would generate a css variable of the name --colors-brand1-primary
.
Because of the way DEFAULT
works, it is possible to have naming collisions. It is on the user of this plugin to ensure that none happen.
require('tailwindcss-themer')({
defaultTheme: {
extend: {
colors: {
brand1: {
DEFAULT: {
primary: {
DEFAULT: 'red'
}
},
primary: 'blue'
}
}
}
}
// ...
})
colors.brand1.DEFAULT.primary.DEFAULT
and colors.brand1.primary
both would generate a css variable named --colors-brand1-primary
. See Default key for more details.
If anywhere in the path, an array is encountered, the index is used in the generated css variable name.
require('tailwindcss-themer')({
defaultTheme: {
extend: {
fontFamily: {
title: ['ui-sans-serif', 'system-ui']
}
}
}
// ...
})
The example above creates the following css variables:
:root {
--font-family-title-0: ui-sans-serif;
--font-family-title-1: system-ui;
}
By default, the config in the defaultTheme
section of the config will apply (i.e. if no class is applied).
Right now, the only way to enable a named theme is to apply a class of the name of the theme you want to enable. I'm open to configuring a theme to activate on other conditions like media queries. If you want this, feel free to open up an issue.
require('tailwindcss-themer')({
defaultTheme: {
extend: {
colors: {
primary: 'red'
}
}
},
themes: [
{
name: 'dark',
extend: {
colors: {
primary: 'blue'
}
}
}
]
})
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
</head>
<body>
<!-- The default theme config would apply here -->
<h1 class="text-primary">Hello world!</h1>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
</head>
<body class="dark">
<!-- The "dark" config would apply here -->
<h1 class="text-primary">Hello world!</h1>
</body>
</html>
In order to prevent a flash of unstyled content, you need to make sure that the class is applied before the first paint. Josh Comeau writes a great article about this.
Because this plugin enables themes based on existance of classes, it is possible to have multiple themes enabled at the same time. They can be overlapping or not. Its all up to how you apply the classes!
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
</head>
<body>
<div class="theme1">
<!-- everything in this div will have theme1 styles -->
</div>
<div class="theme2">
<!-- everything in this div will have theme2 styles -->
</div>
<div class="theme1 theme2">
<!-- everything in this div will have both styles applied -->
<!-- which one has higher specificity is determined by the order of the themes in the "themes" section of the config -->
</div>
</body>
</html>
If you apply two themes at the same time, the specificity is determined by the order of the themes in the themes
array of the config. Later defined themes override earlier configs i.e. theme in index 1 takes precedence over theme in index 0.
require('tailwindcss-themer')({
defaultTheme: {
extend: {
colors: {
primary: 'red'
},
fontFamily: {
title: 'Helvetica'
}
}
},
themes: [
{
name: 'theme1',
extend: {
// ...
}
},
{
// this theme will win out over theme1 if an element has both theme1 and theme2 clases on it because it is defined later in the "themes" array
name: 'theme2',
extend: {
// ...
}
}
]
})
This plugin comes with types. In order to take advantage of them, make sure the files that use this plugin are type checked. For most use cases, this means making sure your tailwind.config.js
file is type checked. The easiest way to do this is by adding //@ts-check
at the top of the file. See the typescript example for a reference implementation.
You may need to bring in types for anything else you import though. e.g. if you import anything from tailwind, you should install @types/tailwindcss
(e.g. yarn add -D @types/tailwindcss
). Another option is to create a declaration file that contains module definitions for anything you import. The typescript docs go further into this.
Those styles are probably getting purged which happens by default in tailwindcss. Read the tailwind docs on how to control what gets purged and what doesn't for details on how this works.
If you're expecting the defaultTheme
to automatically contain tailwind defaults implicitly, read the section on Overwriting tailwind defaults for how to do this properly.
I'm open to discussion. Feel free to open up an issue.
Was this documentation insufficient for you?
Was it confusing?
Was it ... dare I say ... inaccurate?
If any of the above describes your feelings of this documentation. Feel free to open up an issue.