Web InnoventixFreeCode

Gradient Divider

Original · free

gradient/fading divider

byWeb InnoventixReact + Tailwind
divgradientdividers
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/div-gradient.json
div-gradient.tsx
"use client";

import { useId, useState } from "react";
import { motion, useReducedMotion } from "motion/react";

type PaletteId = "aurora" | "ember" | "meadow" | "nightfall";
type FadeId = "both" | "left" | "right" | "full";

interface Palette {
  id: PaletteId;
  name: string;
  note: string;
  stops: [string, string, string];
}

interface DividerProps {
  stops: [string, string, string];
  thickness: number;
  fade: FadeId;
  glow: boolean;
  animate: boolean;
  separator?: boolean;
}

const PALETTE_MAP: Record<PaletteId, Palette> = {
  aurora: {
    id: "aurora",
    name: "Aurora",
    note: "Indigo → violet → sky",
    stops: ["#6366f1", "#8b5cf6", "#38bdf8"],
  },
  ember: {
    id: "ember",
    name: "Ember",
    note: "Rose → coral → amber",
    stops: ["#f43f5e", "#fb7185", "#f59e0b"],
  },
  meadow: {
    id: "meadow",
    name: "Meadow",
    note: "Emerald → mint → sky",
    stops: ["#059669", "#34d399", "#38bdf8"],
  },
  nightfall: {
    id: "nightfall",
    name: "Nightfall",
    note: "Slate → steel → mist",
    stops: ["#475569", "#94a3b8", "#e2e8f0"],
  },
};

const PALETTE_ORDER: PaletteId[] = ["aurora", "ember", "meadow", "nightfall"];

const MASKS: Record<FadeId, string> = {
  both: "linear-gradient(90deg, transparent, #000 18%, #000 82%, transparent)",
  left: "linear-gradient(90deg, transparent, #000 45%)",
  right: "linear-gradient(90deg, #000 55%, transparent)",
  full: "linear-gradient(90deg, #000, #000)",
};

const FADE_OPTIONS: { id: FadeId; label: string }[] = [
  { id: "both", label: "Both edges" },
  { id: "left", label: "Fade in" },
  { id: "right", label: "Fade out" },
  { id: "full", label: "Solid" },
];

const KEYFRAMES = `
@keyframes gd_sweep {
  0% { transform: translateX(-130%); }
  100% { transform: translateX(430%); }
}
@keyframes gd_pulse {
  0%, 100% { opacity: .4; }
  50% { opacity: .85; }
}
.gd-sweep-anim { animation: gd_sweep 3.4s linear infinite; }
.gd-glow-anim { animation: gd_pulse 3.4s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .gd-sweep-anim, .gd-glow-anim { animation: none !important; }
}
`;

function Divider({ stops, thickness, fade, glow, animate, separator }: DividerProps) {
  const gradient = `linear-gradient(90deg, ${stops[0]}, ${stops[1]}, ${stops[2]})`;
  const mask = MASKS[fade];
  return (
    <div
      className="relative w-full"
      style={{ height: `${thickness}px` }}
      role={separator ? "separator" : undefined}
      aria-orientation={separator ? "horizontal" : undefined}
      aria-hidden={separator ? undefined : true}
    >
      <div
        className="absolute inset-0 rounded-full"
        style={{ backgroundImage: gradient, WebkitMaskImage: mask, maskImage: mask }}
      />
      {glow ? (
        <div
          className={`absolute inset-0 rounded-full blur-[4px] ${animate ? "gd-glow-anim" : "opacity-60"}`}
          style={{ backgroundImage: gradient, WebkitMaskImage: mask, maskImage: mask }}
        />
      ) : null}
      {glow && animate ? (
        <div
          className="pointer-events-none absolute inset-0 overflow-hidden rounded-full"
          style={{ WebkitMaskImage: mask, maskImage: mask }}
        >
          <div
            className="gd-sweep-anim absolute inset-y-0 -left-1/4 w-1/4 rounded-full"
            style={{
              backgroundImage:
                "linear-gradient(90deg, transparent, rgba(255,255,255,0.95), transparent)",
            }}
          />
        </div>
      ) : null}
    </div>
  );
}

function Swatch({ stops }: { stops: [string, string, string] }) {
  return (
    <span
      aria-hidden="true"
      className="h-4 w-8 shrink-0 rounded-full ring-1 ring-inset ring-black/10 dark:ring-white/15"
      style={{ backgroundImage: `linear-gradient(90deg, ${stops[0]}, ${stops[1]}, ${stops[2]})` }}
    />
  );
}

export default function GradientDivider() {
  const uid = useId();
  const reduced = useReducedMotion();
  const animate = !reduced;

  const [palette, setPalette] = useState<PaletteId>("aurora");
  const [fade, setFade] = useState<FadeId>("both");
  const [thickness, setThickness] = useState<number>(3);
  const [glow, setGlow] = useState<boolean>(true);
  const [copied, setCopied] = useState<boolean>(false);

  const active = PALETTE_MAP[palette];
  const cssLine = `background: linear-gradient(90deg, ${active.stops.join(", ")});`;

  async function copyCss() {
    try {
      await navigator.clipboard.writeText(cssLine);
      setCopied(true);
      window.setTimeout(() => setCopied(false), 1600);
    } catch {
      setCopied(false);
    }
  }

  const focusRing =
    "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900";

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-4 py-20 text-slate-900 sm:px-6 lg:px-8 dark:bg-slate-950 dark:text-slate-100">
      <style>{KEYFRAMES}</style>

      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0"
        style={{
          backgroundImage:
            "radial-gradient(60rem 30rem at 15% -10%, rgba(99,102,241,0.12), transparent 60%), radial-gradient(50rem 28rem at 90% 0%, rgba(56,189,248,0.10), transparent 55%)",
        }}
      />

      <div className="relative mx-auto max-w-4xl">
        <header className="mb-10 text-center">
          <p className="text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
            Dividers / Gradient
          </p>
          <h2 className="mt-3 text-3xl font-semibold tracking-tight sm:text-4xl">
            Fading gradient divider
          </h2>
          <p className="mx-auto mt-4 max-w-xl text-base leading-relaxed text-slate-600 dark:text-slate-400">
            A horizontal rule that dissolves into the page instead of cutting across it. Tune the
            palette, weight, fade, and shimmer &mdash; then copy the CSS.
          </p>
        </header>

        <motion.div
          initial={reduced ? false : { opacity: 0, y: 14 }}
          whileInView={reduced ? undefined : { opacity: 1, y: 0 }}
          viewport={{ once: true, margin: "-80px" }}
          transition={{ duration: 0.5, ease: "easeOut" }}
          className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8 dark:border-slate-800 dark:bg-slate-900"
        >
          <p className="text-[15px] leading-relaxed text-slate-700 dark:text-slate-300">
            Momentum compounds quietly. The teams that ship every week rarely feel heroic on any
            single day &mdash; they just refuse to let the backlog calcify.
          </p>

          <div className="my-7">
            <Divider
              stops={active.stops}
              thickness={thickness}
              fade={fade}
              glow={glow}
              animate={animate}
              separator
            />
          </div>

          <p className="text-[15px] leading-relaxed text-slate-700 dark:text-slate-300">
            That discipline is invisible in a demo and obvious by the quarter. A divider like this
            one marks the seam without shouting over the words around it.
          </p>
        </motion.div>

        <div className="mt-6 grid gap-5 rounded-2xl border border-slate-200 bg-white p-6 sm:p-7 dark:border-slate-800 dark:bg-slate-900">
          <fieldset>
            <legend className="mb-3 text-sm font-semibold text-slate-800 dark:text-slate-200">
              Palette
            </legend>
            <div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
              {PALETTE_ORDER.map((id) => {
                const p = PALETTE_MAP[id];
                const checked = palette === id;
                return (
                  <label key={id} className="cursor-pointer">
                    <input
                      type="radio"
                      name={`${uid}-palette`}
                      value={id}
                      checked={checked}
                      onChange={() => setPalette(id)}
                      className="peer sr-only"
                    />
                    <span
                      className="flex flex-col gap-2 rounded-xl border border-slate-200 bg-slate-50 p-3 text-left transition-colors peer-checked:border-indigo-500 peer-checked:bg-indigo-50 peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-800/60 dark:peer-checked:border-indigo-400 dark:peer-checked:bg-indigo-500/10 dark:peer-focus-visible:ring-offset-slate-900"
                    >
                      <Swatch stops={p.stops} />
                      <span className="text-sm font-medium text-slate-800 dark:text-slate-100">
                        {p.name}
                      </span>
                      <span className="text-xs text-slate-500 dark:text-slate-400">{p.note}</span>
                    </span>
                  </label>
                );
              })}
            </div>
          </fieldset>

          <div className="grid gap-5 sm:grid-cols-2">
            <fieldset>
              <legend className="mb-3 text-sm font-semibold text-slate-800 dark:text-slate-200">
                Fade
              </legend>
              <div className="grid grid-cols-2 gap-2">
                {FADE_OPTIONS.map((f) => {
                  const checked = fade === f.id;
                  return (
                    <label key={f.id} className="cursor-pointer">
                      <input
                        type="radio"
                        name={`${uid}-fade`}
                        value={f.id}
                        checked={checked}
                        onChange={() => setFade(f.id)}
                        className="peer sr-only"
                      />
                      <span className="block rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-center text-sm text-slate-700 transition-colors peer-checked:border-indigo-500 peer-checked:bg-indigo-50 peer-checked:font-medium peer-checked:text-indigo-700 peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-300 dark:peer-checked:border-indigo-400 dark:peer-checked:bg-indigo-500/10 dark:peer-checked:text-indigo-300 dark:peer-focus-visible:ring-offset-slate-900">
                        {f.label}
                      </span>
                    </label>
                  );
                })}
              </div>
            </fieldset>

            <div className="flex flex-col gap-5">
              <div>
                <div className="mb-3 flex items-center justify-between">
                  <label
                    htmlFor={`${uid}-thickness`}
                    className="text-sm font-semibold text-slate-800 dark:text-slate-200"
                  >
                    Thickness
                  </label>
                  <span className="rounded-md bg-slate-100 px-2 py-0.5 text-xs font-medium tabular-nums text-slate-600 dark:bg-slate-800 dark:text-slate-300">
                    {thickness}px
                  </span>
                </div>
                <input
                  id={`${uid}-thickness`}
                  type="range"
                  min={1}
                  max={8}
                  step={1}
                  value={thickness}
                  onChange={(e) => setThickness(Number(e.target.value))}
                  aria-valuetext={`${thickness} pixels`}
                  className={`w-full cursor-pointer accent-indigo-500 ${focusRing} rounded-full`}
                />
              </div>

              <div className="flex items-center justify-between">
                <span
                  id={`${uid}-glow-label`}
                  className="text-sm font-semibold text-slate-800 dark:text-slate-200"
                >
                  Animated shimmer
                </span>
                <button
                  type="button"
                  role="switch"
                  aria-checked={glow}
                  aria-labelledby={`${uid}-glow-label`}
                  onClick={() => setGlow((g) => !g)}
                  className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors ${
                    glow ? "bg-indigo-500" : "bg-slate-300 dark:bg-slate-700"
                  } ${focusRing}`}
                >
                  <span
                    className={`inline-block h-5 w-5 transform rounded-full bg-white shadow transition-transform ${
                      glow ? "translate-x-5" : "translate-x-0.5"
                    }`}
                  />
                </button>
              </div>
            </div>
          </div>

          <div className="flex flex-col gap-3 rounded-xl border border-slate-200 bg-slate-50 p-3 sm:flex-row sm:items-center sm:justify-between dark:border-slate-700 dark:bg-slate-800/50">
            <code className="overflow-x-auto whitespace-nowrap font-mono text-xs text-slate-600 dark:text-slate-300">
              {cssLine}
            </code>
            <button
              type="button"
              onClick={copyCss}
              className={`inline-flex shrink-0 items-center gap-1.5 rounded-lg bg-slate-900 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-slate-700 ${focusRing} dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200`}
            >
              {copied ? (
                <svg viewBox="0 0 20 20" className="h-3.5 w-3.5" aria-hidden="true">
                  <path
                    d="M4 10.5l4 4 8-8.5"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="2"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  />
                </svg>
              ) : (
                <svg viewBox="0 0 20 20" className="h-3.5 w-3.5" aria-hidden="true">
                  <rect
                    x="7"
                    y="7"
                    width="9"
                    height="9"
                    rx="1.5"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="1.6"
                  />
                  <path
                    d="M13 7V5.5A1.5 1.5 0 0011.5 4h-6A1.5 1.5 0 004 5.5v6A1.5 1.5 0 005.5 13H7"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="1.6"
                    strokeLinecap="round"
                  />
                </svg>
              )}
              {copied ? "Copied" : "Copy CSS"}
            </button>
          </div>
          <span aria-live="polite" className="sr-only">
            {copied ? "CSS copied to clipboard" : ""}
          </span>
        </div>

        <div className="mt-10">
          <p className="mb-5 text-center text-xs font-semibold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-500">
            Every palette, at a glance
          </p>
          <ul className="space-y-6">
            {PALETTE_ORDER.map((id) => {
              const p = PALETTE_MAP[id];
              return (
                <li key={id} className="flex flex-col gap-2">
                  <div className="flex items-center justify-between px-1">
                    <span className="text-sm font-medium text-slate-700 dark:text-slate-300">
                      {p.name}
                    </span>
                    <span className="text-xs text-slate-400 dark:text-slate-500">{p.note}</span>
                  </div>
                  <Divider
                    stops={p.stops}
                    thickness={3}
                    fade="both"
                    glow={id === palette}
                    animate={animate}
                  />
                </li>
              );
            })}
          </ul>
        </div>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →