Web InnoventixFreeCode

Border Gradient Button

Original · free

Buttons with an animated conic gradient border.

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

import {
  useState,
  useRef,
  useCallback,
  type ButtonHTMLAttributes,
  type ReactNode,
} from "react";
import { motion, AnimatePresence, useReducedMotion } from "motion/react";

/* ----------------------------------------------------------------------- */
/*  Sizing tokens                                                          */
/* ----------------------------------------------------------------------- */

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

const SIZES: Record<
  Size,
  { outer: string; inner: string; pad: string; icon: string; text: string }
> = {
  sm: {
    outer: "rounded-[10px]",
    inner: "rounded-[8.5px]",
    pad: "px-3.5 py-1.5",
    icon: "p-2",
    text: "text-xs",
  },
  md: {
    outer: "rounded-[13px]",
    inner: "rounded-[11.5px]",
    pad: "px-5 py-2.5",
    icon: "p-2.5",
    text: "text-sm",
  },
  lg: {
    outer: "rounded-[16px]",
    inner: "rounded-[14.5px]",
    pad: "px-7 py-3.5",
    icon: "p-3",
    text: "text-base",
  },
};

const GRADIENTS = {
  violet: "conic-gradient(from 0deg, #6366f1, #8b5cf6, #d946ef, #8b5cf6, #6366f1)",
  aqua: "conic-gradient(from 0deg, #10b981, #22d3ee, #0ea5e9, #22d3ee, #10b981)",
  ember: "conic-gradient(from 0deg, #f59e0b, #fb7185, #f43f5e, #fb7185, #f59e0b)",
  github: "conic-gradient(from 0deg, #334155, #94a3b8, #475569, #334155)",
  google:
    "conic-gradient(from 0deg, #4285F4, #EA4335, #FBBC05, #34A853, #4285F4)",
  x: "conic-gradient(from 0deg, #0f172a, #475569, #94a3b8, #0f172a)",
} as const;

/* ----------------------------------------------------------------------- */
/*  Core button — animated conic-gradient border                          */
/* ----------------------------------------------------------------------- */

interface GradientButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  size?: Size;
  gradient?: string;
  glow?: boolean;
  iconOnly?: boolean;
  children: ReactNode;
}

function GradientButton({
  size = "md",
  gradient = GRADIENTS.violet,
  glow = true,
  iconOnly = false,
  className = "",
  children,
  disabled,
  ...rest
}: GradientButtonProps) {
  const s = SIZES[size];

  return (
    <button
      disabled={disabled}
      className={[
        "group relative inline-flex select-none items-center justify-center p-[1.5px] isolate",
        s.outer,
        "transition-transform duration-150 ease-out active:scale-[0.96]",
        "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500/70",
        "focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950",
        "disabled:pointer-events-none disabled:opacity-50",
        className,
      ].join(" ")}
      {...rest}
    >
      {/* rotating gradient ring (clipped to the 1.5px border) */}
      <span
        aria-hidden="true"
        className={`absolute inset-0 overflow-hidden ${s.outer}`}
      >
        <span
          className="bgbg-spin"
          style={{
            background: disabled
              ? "conic-gradient(#94a3b8, #cbd5e1, #94a3b8)"
              : gradient,
          }}
        />
      </span>

      {/* soft halo, hover only */}
      {glow && !disabled && (
        <span
          aria-hidden="true"
          className={`pointer-events-none absolute inset-0 -z-10 opacity-0 transition-opacity duration-300 group-hover:opacity-70 ${s.outer}`}
        >
          <span className="bgbg-spin bgbg-glow" style={{ background: gradient }} />
        </span>
      )}

      {/* content plate */}
      <span
        className={[
          "relative z-10 inline-flex items-center justify-center gap-2 font-semibold tracking-tight",
          s.inner,
          iconOnly ? s.icon : s.pad,
          s.text,
          "bg-white text-slate-900 transition-colors duration-200 group-hover:bg-slate-50",
          "dark:bg-slate-950 dark:text-slate-50 dark:group-hover:bg-slate-900",
        ].join(" ")}
      >
        {children}
      </span>
    </button>
  );
}

/* ----------------------------------------------------------------------- */
/*  Inline glyphs                                                          */
/* ----------------------------------------------------------------------- */

type IconProps = { className?: string };

const Sparkle = ({ className = "h-4 w-4" }: IconProps) => (
  <svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden="true">
    <path d="M12 2l1.9 5.6a3 3 0 0 0 1.9 1.9L21.5 11l-5.7 1.5a3 3 0 0 0-1.9 1.9L12 20l-1.9-5.6a3 3 0 0 0-1.9-1.9L2.5 11l5.7-1.5a3 3 0 0 0 1.9-1.9L12 2z" />
  </svg>
);

const ArrowRight = ({ className = "h-4 w-4" }: IconProps) => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
    <path d="M5 12h14M13 6l6 6-6 6" />
  </svg>
);

const Plus = ({ className = "h-4 w-4" }: IconProps) => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" className={className} aria-hidden="true">
    <path d="M12 5v14M5 12h14" />
  </svg>
);

const Check = ({ className = "h-4 w-4" }: IconProps) => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
    <path d="M4 12.5l5 5 11-11" />
  </svg>
);

const Copy = ({ className = "h-4 w-4" }: IconProps) => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className} 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>
);

const Heart = ({ className = "h-4 w-4", filled = false }: IconProps & { filled?: boolean }) => (
  <svg viewBox="0 0 24 24" fill={filled ? "currentColor" : "none"} stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
    <path d="M20.8 5.6a5 5 0 0 0-7.1 0L12 7.3l-1.7-1.7a5 5 0 1 0-7.1 7.1L12 21.5l8.8-8.8a5 5 0 0 0 0-7.1z" />
  </svg>
);

const Gear = ({ className = "h-5 w-5" }: IconProps) => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
    <circle cx="12" cy="12" r="3.2" />
    <path d="M19.4 15a1.6 1.6 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.6 1.6 0 0 0-2.7 1.1V21a2 2 0 1 1-4 0v-.1A1.6 1.6 0 0 0 6.6 19.4l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1A1.6 1.6 0 0 0 3 15a2 2 0 1 1 0-4h.1A1.6 1.6 0 0 0 4.6 8.4l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1A1.6 1.6 0 0 0 9 6V5a2 2 0 1 1 4 0v.1a1.6 1.6 0 0 0 2.7 1.1l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1A1.6 1.6 0 0 0 21 11a2 2 0 1 1 0 4h-.1a1.6 1.6 0 0 0-1.5 1z" />
  </svg>
);

const Spinner = ({ className = "h-4 w-4" }: IconProps) => (
  <svg viewBox="0 0 24 24" fill="none" className={`${className} animate-spin motion-reduce:animate-none`} aria-hidden="true">
    <circle cx="12" cy="12" r="9" stroke="currentColor" strokeOpacity="0.25" strokeWidth="3" />
    <path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />
  </svg>
);

const GitHubGlyph = ({ className = "h-4 w-4" }: IconProps) => (
  <svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden="true">
    <path d="M12 1.5A10.5 10.5 0 0 0 8.7 22c.5.1.7-.2.7-.5v-1.8c-2.9.6-3.5-1.4-3.5-1.4-.5-1.2-1.2-1.5-1.2-1.5-.9-.6.1-.6.1-.6 1 .1 1.6 1 1.6 1 .9 1.6 2.4 1.1 3 .9.1-.7.4-1.1.6-1.4-2.3-.3-4.7-1.2-4.7-5.1 0-1.1.4-2 1-2.7-.1-.3-.5-1.3.1-2.7 0 0 .9-.3 2.8 1a9.4 9.4 0 0 1 5 0c1.9-1.3 2.8-1 2.8-1 .6 1.4.2 2.4.1 2.7.6.7 1 1.6 1 2.7 0 3.9-2.4 4.8-4.7 5.1.4.3.7.9.7 1.9v2.8c0 .3.2.6.7.5A10.5 10.5 0 0 0 12 1.5z" />
  </svg>
);

const GoogleGlyph = ({ className = "h-4 w-4" }: IconProps) => (
  <svg viewBox="0 0 24 24" className={className} aria-hidden="true">
    <path fill="#4285F4" d="M23 12.3c0-.8-.1-1.5-.2-2.3H12v4.5h6.2a5.3 5.3 0 0 1-2.3 3.5v2.9h3.7c2.2-2 3.4-5 3.4-8.6z" />
    <path fill="#34A853" d="M12 24c3.1 0 5.7-1 7.6-2.8l-3.7-2.9c-1 .7-2.4 1.1-3.9 1.1-3 0-5.5-2-6.4-4.7H1.8v3c1.9 3.8 5.8 6.3 10.2 6.3z" />
    <path fill="#FBBC05" d="M5.6 14.7a7.2 7.2 0 0 1 0-4.6v-3H1.8a12 12 0 0 0 0 10.6l3.8-3z" />
    <path fill="#EA4335" d="M12 4.8c1.7 0 3.2.6 4.4 1.7l3.3-3.3A11.5 11.5 0 0 0 12 0C7.6 0 3.7 2.5 1.8 6.3l3.8 3C6.5 6.7 9 4.8 12 4.8z" />
  </svg>
);

const XGlyph = ({ className = "h-4 w-4" }: IconProps) => (
  <svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden="true">
    <path d="M18.2 2h3.3l-7.2 8.2L23 22h-6.6l-5.2-6.8L5.2 22H1.9l7.7-8.8L1 2h6.8l4.7 6.2L18.2 2zm-1.2 18h1.8L7.1 3.9H5.2L17 20z" />
  </svg>
);

/* ----------------------------------------------------------------------- */
/*  Showcase                                                               */
/* ----------------------------------------------------------------------- */

function Label({ children }: { children: ReactNode }) {
  return (
    <p className="mb-4 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400 dark:text-slate-500">
      {children}
    </p>
  );
}

export default function BtnBorderGradient() {
  const reduce = useReducedMotion();

  const [following, setFollowing] = useState(false);
  const [liked, setLiked] = useState(false);
  const [likes, setLikes] = useState(248);
  const [copied, setCopied] = useState(false);
  const [loading, setLoading] = useState(false);
  const copyTimer = useRef<ReturnType<typeof setTimeout> | null>(null);

  const onCopy = useCallback(async () => {
    try {
      await navigator.clipboard.writeText("npx create-next-app@latest");
      setCopied(true);
      if (copyTimer.current) clearTimeout(copyTimer.current);
      copyTimer.current = setTimeout(() => setCopied(false), 1800);
    } catch {
      /* clipboard unavailable */
    }
  }, []);

  const onLike = useCallback(() => {
    setLiked((v) => !v);
    setLikes((n) => (liked ? n - 1 : n + 1));
  }, [liked]);

  const onRun = useCallback(() => {
    if (loading) return;
    setLoading(true);
    setTimeout(() => setLoading(false), 1700);
  }, [loading]);

  return (
    <section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 to-white px-4 py-16 sm:py-24 dark:from-slate-950 dark:to-slate-900">
      <style>{`
        @keyframes bgbg-rotate {
          from { transform: translate(-50%, -50%) rotate(0deg); }
          to   { transform: translate(-50%, -50%) rotate(360deg); }
        }
        .bgbg-spin {
          position: absolute;
          top: 50%;
          left: 50%;
          width: 160%;
          aspect-ratio: 1 / 1;
          transform: translate(-50%, -50%);
          animation: bgbg-rotate var(--bgbg-dur, 4s) linear infinite;
        }
        .group:hover .bgbg-spin { --bgbg-dur: 1.6s; }
        .bgbg-glow { filter: blur(10px); }
        @media (prefers-reduced-motion: reduce) {
          .bgbg-spin { animation: none; }
          .group:hover .bgbg-spin { --bgbg-dur: 4s; }
        }
      `}</style>

      <div className="mx-auto max-w-4xl">
        <header className="mb-14 text-center">
          <span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white/70 px-3 py-1 text-xs font-medium text-slate-600 backdrop-blur dark:border-slate-700 dark:bg-slate-900/70 dark:text-slate-300">
            <Sparkle className="h-3.5 w-3.5 text-violet-500" />
            Animated conic border
          </span>
          <h2 className="mt-5 bg-gradient-to-r from-slate-900 to-slate-600 bg-clip-text text-3xl font-bold tracking-tight text-transparent sm:text-4xl dark:from-white dark:to-slate-400">
            Gradient Border Buttons
          </h2>
          <p className="mx-auto mt-3 max-w-md text-sm text-slate-500 dark:text-slate-400">
            A rotating conic ring that speeds up on hover. Tactile, focus-visible,
            and fully reduced-motion aware.
          </p>
        </header>

        <div className="space-y-14">
          {/* Sizes */}
          <div>
            <Label>Sizes</Label>
            <div className="flex flex-wrap items-center gap-4">
              <GradientButton size="sm">
                Get started <ArrowRight className="h-3.5 w-3.5" />
              </GradientButton>
              <GradientButton size="md">
                Get started <ArrowRight className="h-4 w-4" />
              </GradientButton>
              <GradientButton size="lg">
                Get started <ArrowRight className="h-5 w-5" />
              </GradientButton>
            </div>
          </div>

          {/* Palettes */}
          <div>
            <Label>Palettes</Label>
            <div className="flex flex-wrap items-center gap-4">
              <GradientButton gradient={GRADIENTS.violet}>
                <Sparkle /> Deploy Nova UI
              </GradientButton>
              <GradientButton gradient={GRADIENTS.aqua}>
                <Sparkle /> Sync Aurora DB
              </GradientButton>
              <GradientButton gradient={GRADIENTS.ember}>
                <Sparkle /> Launch Comet
              </GradientButton>
            </div>
          </div>

          {/* Interactive states */}
          <div>
            <Label>Interactive</Label>
            <div className="flex flex-wrap items-center gap-4">
              {/* Toggle — aria-pressed */}
              <GradientButton
                aria-pressed={following}
                gradient={following ? GRADIENTS.aqua : GRADIENTS.violet}
                onClick={() => setFollowing((v) => !v)}
              >
                {following ? <Check /> : <Plus />}
                {following ? "Following" : "Follow"}
              </GradientButton>

              {/* Like — aria-pressed + live count */}
              <GradientButton
                aria-pressed={liked}
                gradient={liked ? GRADIENTS.ember : GRADIENTS.violet}
                onClick={onLike}
              >
                <motion.span
                  key={liked ? "on" : "off"}
                  initial={reduce ? false : { scale: 0.6 }}
                  animate={{ scale: 1 }}
                  transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 500, damping: 16 }}
                  className={liked ? "text-rose-500" : ""}
                >
                  <Heart filled={liked} />
                </motion.span>
                <span className="tabular-nums">{likes}</span>
              </GradientButton>

              {/* Copy — clipboard */}
              <GradientButton
                gradient={copied ? GRADIENTS.aqua : GRADIENTS.violet}
                onClick={onCopy}
                aria-label="Copy install command"
              >
                <AnimatePresence mode="wait" initial={false}>
                  {copied ? (
                    <motion.span
                      key="done"
                      initial={reduce ? false : { opacity: 0, y: 6 }}
                      animate={{ opacity: 1, y: 0 }}
                      exit={reduce ? { opacity: 0 } : { opacity: 0, y: -6 }}
                      transition={{ duration: reduce ? 0 : 0.18 }}
                      className="inline-flex items-center gap-2 text-emerald-600 dark:text-emerald-400"
                    >
                      <Check /> Copied!
                    </motion.span>
                  ) : (
                    <motion.span
                      key="copy"
                      initial={reduce ? false : { opacity: 0, y: 6 }}
                      animate={{ opacity: 1, y: 0 }}
                      exit={reduce ? { opacity: 0 } : { opacity: 0, y: -6 }}
                      transition={{ duration: reduce ? 0 : 0.18 }}
                      className="inline-flex items-center gap-2 font-mono text-[13px]"
                    >
                      <Copy /> npx create-next-app
                    </motion.span>
                  )}
                </AnimatePresence>
              </GradientButton>

              {/* Loading */}
              <GradientButton
                gradient={GRADIENTS.violet}
                onClick={onRun}
                disabled={loading}
                aria-busy={loading}
              >
                {loading ? (
                  <>
                    <Spinner /> Building…
                  </>
                ) : (
                  <>
                    <Sparkle /> Run build
                  </>
                )}
              </GradientButton>
            </div>
          </div>

          {/* Icon-only + disabled */}
          <div>
            <Label>Icon-only &amp; disabled</Label>
            <div className="flex flex-wrap items-center gap-4">
              <GradientButton size="sm" iconOnly aria-label="Open settings" gradient={GRADIENTS.violet}>
                <Gear className="h-4 w-4" />
              </GradientButton>
              <GradientButton size="md" iconOnly aria-label="Open settings" gradient={GRADIENTS.aqua}>
                <Gear className="h-5 w-5" />
              </GradientButton>
              <GradientButton size="lg" iconOnly aria-label="Open settings" gradient={GRADIENTS.ember}>
                <Gear className="h-6 w-6" />
              </GradientButton>
              <GradientButton disabled glow={false}>
                <Sparkle /> Unavailable
              </GradientButton>
            </div>
          </div>

          {/* Social */}
          <div>
            <Label>Social</Label>
            <div className="flex flex-wrap items-center gap-4">
              <GradientButton
                gradient={GRADIENTS.github}
                aria-label="Continue with GitHub"
                onClick={() => window.open("https://github.com", "_blank", "noopener")}
              >
                <GitHubGlyph /> Continue with GitHub
              </GradientButton>
              <GradientButton
                gradient={GRADIENTS.google}
                aria-label="Continue with Google"
                onClick={() => window.open("https://google.com", "_blank", "noopener")}
              >
                <GoogleGlyph /> Continue with Google
              </GradientButton>
              <GradientButton
                gradient={GRADIENTS.x}
                aria-label="Continue with X"
                onClick={() => window.open("https://x.com", "_blank", "noopener")}
              >
                <XGlyph /> Continue with X
              </GradientButton>
            </div>
          </div>
        </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 →