Creating a tailwind plugin shouldn't be that hard, right?

blankparticle

Blank Particle

Posted on December 9, 2023

Creating a tailwind plugin shouldn't be that hard, right?

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.

A Demo of RealtimeColors.com

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
    }),
  ]
}
Enter fullscreen mode Exit fullscreen mode

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()],
  // ...
};
Enter fullscreen mode Exit fullscreen mode

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*/)],
  //...
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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),
      },
    },
  }),
);
Enter fullscreen mode Exit fullscreen mode

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.

Tailwind Shade Algorithm

RealtimeColors Shade Algorithm

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 {
  /*...*/  
}
Enter fullscreen mode Exit fullscreen mode

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 with hsl,lch or lab. The fix is to use rgba instead of rgb. For other formats than rgba, we can also use <alpha-value> like hsl(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 👋

💖 💪 🙅 🚩
blankparticle
Blank Particle

Posted on December 9, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related