Creating a tailwind plugin shouldn't be that hard, right?
Blank Particle
Posted on December 9, 2023
Recently, I came across a site called RealtimeColors.com, It's a pretty cool site that helps you choose colors for your site and visualize them on a real site.
The site provides an export for tailwind, but it's tedious to copy and set up everything and change the colors later. So, as a developer myself, I spent 2 days automating a task that would hardly take 2 minutes of my life. The result is tailwind-plugin-realtime-colors
. Please consider giving a star to the repo.
Sit tight, as this is gonna be a long ride.
Creating Tailwind Plugins
TailwindCSS provides a Plugin API. It is not that intuitive, to be honest. Here is a snippet they provide on their site.
const plugin = require('tailwindcss/plugin')
module.exports = {
plugins: [
plugin(function({ addUtilities, addComponents, e, config }) {
// Add your custom styles here
}),
]
}
At a glance you can't understand what the f*ck is going on, but here is what It actually means in ES6 syntax.
// tailwind.config.ts
import plugin from "tailwindcss/plugin";
const somePlugin = plugin(({ addUtilites }) => {
addUtilites(/*...*/);
//...
});
export default {
// ...
plugins: [somePlugin()],
// ...
};
Actually creating the plugin
So, I started working on the plugin in a file called realtime-colors.ts
in the project directory. I wanted to achieve this kind of configuration.
import realtimeColors from "./realtime-colors.ts";
export default {
//...
plugins: [realtimeColors("https://www.realtimecolors.com/?colors=e8eef4-050709-a1b9d1-75393b-ab9d54", /*Some config options*/)],
//...
};
I wrote some type definitions and function signatures for the starter.
// realtime-colors.ts
import plugin from "tailwindcss/plugin";
type HexColor = `#${string}`;
type Plugin = ReturnType<typeof plugin>;
export type RealtimeColorOptions = {
colors: {
text: HexColor;
background: HexColor;
primary: HexColor;
secondary: HexColor;
accent: HexColor;
};
// don't worry about these options now
theme: boolean;
shades: (keyof RealtimeColorOptions["colors"])[];
prefix: string;
shadeAlgorithm: keyof typeof availableModifiers;
};
type RealtimeColorOptionsWithoutColor = Omit<RealtimeColorOptions, "colors">;
// ...
function realtimeColors(
config: Pick<RealtimeColorOptions, "colors"> & Partial<RealtimeColorOptionsWithoutColor>,
): Plugin;
function realtimeColors(
url: string,
extraConfig?: Partial<RealtimeColorOptionsWithoutColor>,
): Plugin;
function realtimeColors(
configOrUrl: string | Pick<RealtimeColorOptions, "colors">,
extraConfig?: Partial<RealtimeColorOptionsWithoutColor>,
): Plugin {
// Handle the passed options and return plugin with appropiate config
}
export default realtimeColors;
If you are confused about why there are 3 functions named realtimeColors
I recommend you learn function overloading.
Now I check if the first option is a string
, then I parse the URL, extract the colors and combine it with the optional passed config and defaultConfig
. If the first option is an object then I just combine it with defaultConfig
. You can look at the actual implementation on my GitHub.
Now for the actual plugin, I have an arrow function called realtimeColorsPlugin
.
const realtimeColorsPlugin = plugin.withOptions<RealtimeColorOptions>(
(options) => ({ addBase }) => addBase(getCSS(options)),
(options) => ({
theme: {
extend: {
colors: getTheme(options),
},
},
}),
);
If you want to pass options to the plugin you need to use
plugin.withOptions
getCSS
and getTheme
function
Before we jump into getCSS
and getTheme
function, we need to establish some concepts.
Modifiers
A modifier is an object with keys 50|100|200|300|400|500|600|700|800|900|950
, and have function with signature ([number,number,number]) => [number, number, number]
.
If you haven't guessed already, they take RGB
colors and modifies the color.
As of version 1.1.1
of the plugin It has 2 types of modifiers, one is realtimeColors
which uses the same algorithm as RealtimeColors.com to create the shades of the given colors. Personally, I don't like the shades created by this algorithm. So there is an alternative modifier called tailwind
which is the default for this plugin.
You can use whatever algorithm you like, but remember the color you selected is not present in realtimeColors
shades, but it is present in tailwind
shades as 500
.
Also, by default primary
, secondary
and accent
are shaded. If you also want to shade text
or background
, you can do so by passing the colors you want to shade in shades
array.
See More about the options here.
Theme
The theme
option decides if the colors should automatically adapt to dark/light mode. there is no need to use dark:
as the colors are assigned to CSS variables which change based on dark
class on html
element.
To create the alternate variant of a certain color, invertColor
function is used. I didn't create this function, it was reverse-engineered from RealtimeColors.com.
getCSS
function
This function is responsible for generating the CSS variables. So it returns nothing if theme
is set to false
as the colors are directly embedded in the config.
Otherwise, it generates the values to use in a CSS rgb/hsl/lch/lab
color function based on the config. It also creates the shades with the specified modifiers if needed. Then the variables are injected into :root
and :is(.dark):root
.
The variables are just values without the color function.
:root {
--text: 3, 32, 30;
--background: 246, 254, 253;
--primary-50: 0, 5, 4;
--primary-100: 1, 14, 13;
--primary-200: 2, 29, 25;
--primary-300: 4, 47, 41;
/*...*/
}
:is(.dark):root {
/*...*/
}
At first, I wrote my own functions for color conversion, It was a mess and they were terribly inaccurate. I suggest using an established library like
color-convert
to handle these tasks.
getTheme
function
It is responsible for adding the colors in the tailwind config. If theme
is set to false
then it embeds the color directly in the config. Otherwise, it uses the variables from getCSS
with the proper color function.
For some reason
rgb
doesn't support alpha values with CSS Variables. This problem doesn't happen withhsl
,lch
orlab
. The fix is to usergba
instead ofrgb
. For other formats thanrgba
, we can also use<alpha-value>
likehsl(var(--text) / <alpha-value>)
, even tho it works without it, but it's still recommended
And just like that you have your very own TailwindCSS Plugin. Obviously, I skipped a lot of details and implementations. You can visit my GitHub Repo for the full code.
While you are going to visit the repo, consider giving it a ⭐, It motivates me.
There is going to be a sequel to this blog on how to convert this file into an npm package, bundle it and deploy it to npmjs.com using GitHub Actions. Stay tuned for that.
it's Blank Particle, Signing Out 👋
Posted on December 9, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.