Web InnoventixFreeCode

Gradient Set Button

Original · free

Gradient-filled buttons with a hover shift.

byWeb InnoventixReact + Tailwind
btngradientsetbuttons
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/btn-gradient-set.json
btn-gradient-set.tsx
"use client";

import { useState } from "react";

type Size = "sm" | "md" | "lg";

const sizeMap: Record<Size, string> = {
  sm: "text-sm px-4 py-2 gap-1.5 rounded-lg",
  md: "text-sm px-5 py-2.5 gap-2 rounded-xl",
  lg: "text-base px-7 py-3.5 gap-2.5 rounded-2xl",
};

const iconSizeMap: Record<Size, string> = {
  sm: "h-4 w-4",
  md: "h-4 w-4",
  lg: "h-5 w-5",
};

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

const baseBtn =
  "relative inline-flex select-none items-center justify-center overflow-hidden font-semibold text-white antialiased transition-transform duration-200 will-change-transform active:translate-y-px disabled:cursor-not-allowed disabled:opacity-50 disabled:active:translate-y-0";

function ArrowIcon({ className }: { className: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2.2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="M5 12h14" />
      <path d="M13 6l6 6-6 6" />
    </svg>
  );
}

function SparkIcon({ className }: { className: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill="currentColor"
      aria-hidden="true"
    >
      <path d="M12 2l1.9 5.6a4 4 0 0 0 2.5 2.5L22 12l-5.6 1.9a4 4 0 0 0-2.5 2.5L12 22l-1.9-5.6a4 4 0 0 0-2.5-2.5L2 12l5.6-1.9a4 4 0 0 0 2.5-2.5L12 2z" />
    </svg>
  );
}

function HeartIcon({ className, filled }: { className: string; filled: boolean }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill={filled ? "currentColor" : "none"}
      stroke="currentColor"
      strokeWidth={2.2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="M20.8 5.6a5.4 5.4 0 0 0-7.6 0L12 6.8l-1.2-1.2a5.4 5.4 0 0 0-7.6 7.6L12 21.6l8.8-8.4a5.4 5.4 0 0 0 0-7.6z" />
    </svg>
  );
}

function CheckIcon({ className }: { className: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2.6}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="M20 6L9 17l-5-5" />
    </svg>
  );
}

function CopyIcon({ className }: { className: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2.2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <rect x="9" y="9" width="11" height="11" rx="2" />
      <path d="M5 15V5a2 2 0 0 1 2-2h10" />
    </svg>
  );
}

function Spinner({ className }: { className: string }) {
  return (
    <svg className={className} viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <circle
        cx="12"
        cy="12"
        r="9"
        stroke="currentColor"
        strokeOpacity="0.3"
        strokeWidth="3"
      />
      <path
        d="M21 12a9 9 0 0 0-9-9"
        stroke="currentColor"
        strokeWidth="3"
        strokeLinecap="round"
      />
    </svg>
  );
}

/* Shared "hover shift" gradient shell: an over-sized gradient layer that slides
   its background-position on hover, plus a glossy top highlight for tactility. */
function GradientFace({ gradient }: { gradient: string }) {
  return (
    <>
      <span
        aria-hidden="true"
        className={`gbtn-face absolute inset-0 -z-10 bg-[length:200%_100%] bg-[position:0%_50%] transition-[background-position] duration-500 ease-out ${gradient}`}
      />
      <span
        aria-hidden="true"
        className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-1/2 rounded-[inherit] bg-gradient-to-b from-white/25 to-transparent"
      />
    </>
  );
}

export default function BtnGradientSet() {
  const [size, setSize] = useState<Size>("md");
  const [liked, setLiked] = useState(false);
  const [likeCount, setLikeCount] = useState(1284);
  const [copied, setCopied] = useState(false);
  const [loading, setLoading] = useState(false);
  const [subscribed, setSubscribed] = useState(false);

  const sz = sizeMap[size];
  const isz = iconSizeMap[size];

  function toggleLike() {
    setLiked((prev) => {
      setLikeCount((c) => (prev ? c - 1 : c + 1));
      return !prev;
    });
  }

  async function copyCoupon() {
    try {
      await navigator.clipboard.writeText("INNOV25");
    } catch {
      /* clipboard unavailable — still reflect the intent in UI */
    }
    setCopied(true);
    window.setTimeout(() => setCopied(false), 1800);
  }

  function fakeSubmit() {
    if (loading || subscribed) return;
    setLoading(true);
    window.setTimeout(() => {
      setLoading(false);
      setSubscribed(true);
    }, 1400);
  }

  return (
    <section className="relative w-full bg-slate-50 px-6 py-20 dark:bg-slate-950">
      <style>{`
        @keyframes gbtnShine {
          0% { transform: translateX(-140%) skewX(-18deg); opacity: 0; }
          18% { opacity: 0.9; }
          60% { opacity: 0.9; }
          100% { transform: translateX(140%) skewX(-18deg); opacity: 0; }
        }
        @keyframes gbtnPop {
          0% { transform: scale(1); }
          45% { transform: scale(1.35); }
          100% { transform: scale(1); }
        }
        .gbtn-shine::after {
          content: "";
          position: absolute;
          top: 0;
          left: 0;
          bottom: 0;
          width: 45%;
          background: linear-gradient(90deg, transparent, rgba(255,255,255,0.55), transparent);
          transform: translateX(-140%) skewX(-18deg);
          pointer-events: none;
        }
        .gbtn-shine:hover::after,
        .gbtn-shine:focus-visible::after {
          animation: gbtnShine 0.9s ease-out;
        }
        .group\\/g:hover .gbtn-face,
        .gbtn-face:hover {
          background-position: 100% 50%;
        }
        .gbtn-pop { animation: gbtnPop 0.42s ease-out; }
        @media (prefers-reduced-motion: reduce) {
          .gbtn-face { transition: none !important; }
          .gbtn-shine::after { display: none !important; }
          .gbtn-pop { animation: none !important; }
        }
      `}</style>

      <div className="mx-auto max-w-4xl">
        <header className="mb-12 text-center">
          <p className="mb-3 text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
            FreeCode / Buttons
          </p>
          <h2 className="text-3xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
            Gradient Button Set
          </h2>
          <p className="mx-auto mt-3 max-w-lg text-sm text-slate-600 dark:text-slate-400">
            Gradient-filled buttons with a smooth hover shift, glossy highlight
            and a light sweep — tactile, focus-ringed and fully keyboard
            accessible.
          </p>
        </header>

        {/* Size switcher */}
        <div className="mb-12 flex items-center justify-center gap-2">
          <span className="mr-1 text-xs font-medium text-slate-500 dark:text-slate-400">
            Size
          </span>
          {(["sm", "md", "lg"] as const).map((s) => {
            const active = size === s;
            return (
              <button
                key={s}
                type="button"
                aria-pressed={active}
                onClick={() => setSize(s)}
                className={`rounded-lg px-3 py-1.5 text-xs font-semibold uppercase tracking-wide transition-colors ${focusRing} focus-visible:ring-indigo-500 ${
                  active
                    ? "bg-slate-900 text-white dark:bg-white dark:text-slate-900"
                    : "bg-slate-200 text-slate-600 hover:bg-slate-300 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
                }`}
              >
                {s}
              </button>
            );
          })}
        </div>

        {/* Primary gradient variants */}
        <div className="mb-14 flex flex-wrap items-center justify-center gap-4">
          <button
            type="button"
            className={`group/g gbtn-shine ${baseBtn} ${sz} shadow-lg shadow-indigo-500/30 hover:shadow-xl hover:shadow-indigo-500/40 ${focusRing} focus-visible:ring-indigo-500`}
          >
            <GradientFace gradient="bg-gradient-to-r from-indigo-500 via-violet-500 to-fuchsia-500" />
            Get started free
            <ArrowIcon
              className={`${isz} transition-transform duration-200 group-hover/g:translate-x-0.5`}
            />
          </button>

          <button
            type="button"
            className={`group/g gbtn-shine ${baseBtn} ${sz} shadow-lg shadow-emerald-500/30 hover:shadow-xl hover:shadow-emerald-500/40 ${focusRing} focus-visible:ring-emerald-500`}
          >
            <GradientFace gradient="bg-gradient-to-r from-emerald-500 via-teal-500 to-sky-500" />
            <SparkIcon className={isz} />
            Upgrade to Pro
          </button>

          <button
            type="button"
            className={`group/g gbtn-shine ${baseBtn} ${sz} shadow-lg shadow-rose-500/30 hover:shadow-xl hover:shadow-rose-500/40 ${focusRing} focus-visible:ring-rose-500`}
          >
            <GradientFace gradient="bg-gradient-to-r from-rose-500 via-pink-500 to-amber-400" />
            Start 14-day trial
          </button>
        </div>

        {/* Outline gradient + ghost row */}
        <div className="mb-14 flex flex-wrap items-center justify-center gap-4">
          {/* Gradient-border (outline) button */}
          <button
            type="button"
            className={`group/g relative inline-flex select-none items-center justify-center overflow-hidden rounded-xl bg-gradient-to-r from-indigo-500 via-violet-500 to-fuchsia-500 bg-[length:200%_100%] bg-[position:0%_50%] p-[1.5px] font-semibold transition-[background-position,transform] duration-500 ease-out hover:bg-[position:100%_50%] active:translate-y-px ${focusRing} focus-visible:ring-violet-500`}
          >
            <span
              className={`inline-flex items-center gap-2 rounded-[10px] bg-white ${sz} dark:bg-slate-950`}
            >
              <span className="bg-gradient-to-r from-indigo-600 via-violet-600 to-fuchsia-600 bg-clip-text text-transparent dark:from-indigo-300 dark:via-violet-300 dark:to-fuchsia-300">
                Read the docs
              </span>
            </span>
          </button>

          {/* Icon-only gradient like button */}
          <button
            type="button"
            aria-pressed={liked}
            aria-label={liked ? "Unlike this project" : "Like this project"}
            onClick={toggleLike}
            className={`group/g gbtn-shine ${baseBtn} ${sizeMap[size]} !px-4 shadow-lg shadow-rose-500/30 hover:shadow-xl hover:shadow-rose-500/40 ${focusRing} focus-visible:ring-rose-500`}
          >
            <GradientFace
              gradient={
                liked
                  ? "bg-gradient-to-r from-rose-500 via-pink-500 to-rose-600"
                  : "bg-gradient-to-r from-slate-600 via-slate-700 to-slate-800"
              }
            />
            <HeartIcon
              key={liked ? "on" : "off"}
              filled={liked}
              className={`${isz} ${liked ? "gbtn-pop" : ""}`}
            />
            <span className="tabular-nums">{likeCount.toLocaleString()}</span>
          </button>

          {/* Copy coupon */}
          <button
            type="button"
            onClick={copyCoupon}
            aria-label={copied ? "Coupon code copied" : "Copy coupon code INNOV25"}
            className={`group/g gbtn-shine ${baseBtn} ${sz} shadow-lg shadow-sky-500/30 hover:shadow-xl hover:shadow-sky-500/40 ${focusRing} focus-visible:ring-sky-500`}
          >
            <GradientFace
              gradient={
                copied
                  ? "bg-gradient-to-r from-emerald-500 via-teal-500 to-emerald-600"
                  : "bg-gradient-to-r from-sky-500 via-blue-500 to-indigo-500"
              }
            />
            {copied ? (
              <CheckIcon className={`${isz} gbtn-pop`} />
            ) : (
              <CopyIcon className={isz} />
            )}
            <span className="tabular-nums">{copied ? "Copied!" : "INNOV25"}</span>
          </button>
        </div>

        {/* Async / loading + success states */}
        <div className="flex flex-wrap items-center justify-center gap-4">
          <button
            type="button"
            onClick={fakeSubmit}
            disabled={loading || subscribed}
            aria-live="polite"
            className={`group/g gbtn-shine ${baseBtn} ${sz} min-w-[13rem] shadow-lg shadow-violet-500/30 hover:shadow-xl hover:shadow-violet-500/40 ${focusRing} focus-visible:ring-violet-500`}
          >
            <GradientFace
              gradient={
                subscribed
                  ? "bg-gradient-to-r from-emerald-500 via-teal-500 to-emerald-600"
                  : "bg-gradient-to-r from-violet-500 via-purple-500 to-indigo-500"
              }
            />
            {loading ? (
              <>
                <Spinner className={`${isz} animate-spin`} />
                Subscribing…
              </>
            ) : subscribed ? (
              <>
                <CheckIcon className={`${isz} gbtn-pop`} />
                You&apos;re in!
              </>
            ) : (
              <>
                <SparkIcon className={isz} />
                Subscribe to newsletter
              </>
            )}
          </button>

          {/* Disabled example */}
          <button
            type="button"
            disabled
            className={`${baseBtn} ${sz} shadow-lg`}
          >
            <GradientFace gradient="bg-gradient-to-r from-slate-400 to-slate-500 dark:from-slate-600 dark:to-slate-700" />
            Sold out
          </button>
        </div>
      </div>
    </section>
  );
}

Dependencies

None (React + Tailwind only).

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 →