Creating a Tailwind color picker

A little helper function I found useful in a recent project that allows you to get the Tailwind color closest to a given CSS color value.

What is this

A general guide on taking a hex code and getting an equivalent Tailwind color class. Initially this project came to mind when checking out Sanity’s image palette information which provides hex codes for varying complimenting and contrasting colors for a given image. I thought it would be cool to extend that information with Tailwind classes but that’s for another day!

Try it out

I set up a little UI in Astro for using a color picker to get the closest Tailwind color class name, and I’ll detail how I built it in this post.

You can see this color picker in action here.

bg-black

Thanks to the hex2tailwind package

Most of this helper is just the hex2tailwind package, I simply updated the logic for deleting classes that don’t apply to the lookup.

Code

ColorPicker component

Here’s our Astro component for selecting a color:

<div>
  <fieldset>
    <label for="color-select">Select color:</label>
    <input
      type="color"
      id="color-select"
      class="mx-3 h-48 w-48 hover:cursor-pointer"
    />
  </fieldset>
  // Custom Astro component for showing a swatch given a Tailwind color class
  name
  <ColorSwatch twClassName="black" id="main-swatch" />
</div>
<script>
  // Get the 2 elements above
  const mainSwatch = document.getElementById("main-swatch");
  const colorSelect = document.getElementById("color-select");
  colorSelect?.addEventListener("change", async (e) => {
    const {
      target: { value }, // Color input element returns a hex value regardless of what format is chosen by user
    } = e;

    // Get tw class from netlify function
    // I happened to put this in a serverless function but the logic could also be placed directly inline
    const hex = await fetch(
      `http://localhost:8888/.netlify/functions/tailwind-hex?hex=${encodeURIComponent(
        value
      )}`
    ).then((res) => res.json());

    // Update DOM with new TW class
    const { twClass } = hex; // Get class from function response
    const bgClassName = `bg-${twClass}`; // Construct Tailwind background class
    const currentBg = mainSwatch?.dataset.bgClassName; // ColorSwatch component has a data attribute to pass current class name
    mainSwatch?.classList.replace(currentBg, bgClassName); // Swap our background classes
    mainSwatch.dataset.bgClassName = bgClassName; // Update ColorSwatch data attribute
    // Update text inside main swatch
    const p = mainSwatch?.querySelector("p");
    p.innerText = bgClassName;
  });
</script>

ColorSwatch component

Just used to render a square of whatever Tailwind color class you give it

---
import { twMerge } from "tailwind-merge";
const { twClassName, id } = Astro.props;
const bgClassName = `bg-${twClassName}`;
const classList = twMerge(
  bgClassName,
  "color-swatch h-48 w-48 flex items-end justify-end hover:cursor-pointer"
);
---

<div class={classList} id={id || bgClassName} data-bg-class-name={bgClassName}>
  <p class="bg-black p-2 font-mono">{bgClassName}</p>
</div>

Logic for parsing hex value

Here’s the function comparing the hex code from the color picker to Tailwind classes. As stated above most of this logic is thanks to the hex2tailwind package

import colors from "tailwindcss/colors";
import { flatten } from "flat";

function tailwindReference() {
  // Flattens the color object and adds a "-" delimiter for exact TailwindCSS match
  const flat = flatten(colors, { delimiter: "-" });
  // Collect all colors that aren't hex codes
  const notHex = Object.entries(flat).filter(
    ([key, value]) =>
      !/^#([0-9A-F]{3}){1,2}$/i.test(value) || key !== key.toLowerCase()
  );

  // Delete colors without matching hex codes
  notHex.forEach(([key]) => delete flat[key]);

  return flat;
}

function tailwindMatcher(color: string) {
  // Initialize the color matcher on the flattened Tailwind colors object
  const nearestColor = require("nearest-color").from(tailwindReference());

  // Check if the hex color is correctly formatted and return the closest match, otherwise throw an error.
  if (/^#([0-9A-F]{3}){1,2}$/i.test(color)) {
    return nearestColor(color).name;
  } else {
    throw new Error("Wrong Hex syntax. Please use #xxx or #xxxxxx.");
  }
}
export { tailwindReference, tailwindMatcher };

Serverless function

As shows in the ColorPicker component above, I call the parsing logic from a Netlify function like so

import type { Handler, HandlerEvent } from "@netlify/functions";
import { tailwindMatcher } from "../../utils/tailwind-hex";

const handler: Handler = async (event: HandlerEvent) => {
  const { queryStringParameters } = event;
  const { hex } = queryStringParameters;

  return {
    statusCode: 200,
    body: JSON.stringify({ twClass: tailwindMatcher(hex) }),
  };
};

export { handler };

Gotchas

  • Some hex values just don’t have reasonable matches in Tailwind, so your results may vary.
  • The less saturated a hex, the more likely to get a variation of gray you are
Last updated: 11/24/2023