Gradient Border

Add gradient to border

Gradient Border

CSS borders don't accept gradients directly, so this snippet fakes one with a nesting trick: an outer element carries the gradient and a little padding, and a solid-colored inner element sits on top, leaving only the padded edge of the gradient showing as a "border." On top of that, this component adds a small row of color swatches so you can switch between several preset gradients at runtime. I use this when I want a button or card with an eye-catching colored frame and the ability to preview a few palettes quickly.

import React from "react";
// add classNames conditionally
import cx from "clsx";

const colors = [
  "from-yellow-400 via-red-500 to-pink-500",
  "from-purple-400 via-pink-500 to-red-500",
  "from-yellow-100 via-yellow-300 to-yellow-500",
  "from-green-400 to-blue-500",
  "from-green-500 to-green-700",
  "from-gray-500 via-gray-100 to-gray-500",
];

export const GradientBorder = () => {
  const [colorIndex, setColorIndex] = React.useState(0);

  const gradientColor = colors[colorIndex];

  return (
    <div>
      <div
        className={cx(
          "flex justify-center w-56 p-1 rounded-md h-14 bg-gradient-to-r ",
          gradientColor
        )}
        role="button"
      >
        <div className="flex items-center justify-center flex-grow text-white bg-black rounded">
          <p>Gradient Border</p>
        </div>
      </div>
      <div className="flex mt-10 space-x-4 rounded">
        {colors.map((color, index) => {
          return (
            <div
              key={index}
              className={cx(
                "h-6 w-6 rounded cursor-pointer transform ring-1 ring-offset-2 ring-white ring-offset-black bg-gradient-to-r ",
                color,
                {
                  "ring-opacity-100": colorIndex === index,
                  "ring-opacity-0 hover:ring-opacity-50": colorIndex !== index,
                }
              )}
              onClick={() => setColorIndex(index)}
            ></div>
          );
        })}
      </div>
    </div>
  );
};

How it works

The gradient "border" is the outer div: bg-gradient-to-r plus one of the from-/via-/to- color triples paints a left-to-right gradient, and p-1 gives it one unit of padding. The inner div uses bg-black and flex-grow to fill the middle, so the only gradient you actually see is the 1-unit ring of padding around it, which reads as a border. rounded-md on the outer box and rounded on the inner box keep the corners matched.

State is handled with a single colorIndex from useState. colors is an array of Tailwind gradient class strings, and gradientColor = colors[colorIndex] picks the active one. The clsx helper (imported as cx) merges the static classes with that dynamic gradient string, and in the swatch row it also toggles object-form classes conditionally: the selected swatch gets ring-opacity-100, the others get ring-opacity-0 hover:ring-opacity-50 so they reveal a faint ring on hover. Clicking a swatch calls setColorIndex(index), which re-renders both the preview and the swatches with the new gradient.

Notes & variations

This is Tailwind-specific: the gradient utilities and clsx are required, and the gradient class strings must be statically present so Tailwind's compiler doesn't purge them, listing them in the colors array (as done here) keeps them safe. The inner background (bg-black here) is what defines the "fill" color; switch it to bg-white with dark text for a light theme. Make the border thicker by raising p-1 to p-1.5 or p-2.

A few caveats. The preview element uses role="button" but has no onClick or keyboard handler, it's purely visual here, so if you make it actually interactive, add an onClick, a tabIndex, and key handling so it's reachable by keyboard. The swatches are clickable divs rather than real <button>s; for production I'd switch them to <button> elements (or add tabIndex/onKeyDown and an aria-pressed state) so keyboard and screen-reader users can change the gradient too. Finally, the key={index} on the swatches is fine because the list is static and never reordered.

Live Demo