Border Gradient Button
Original · freeButtons with an animated conic gradient border.
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"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 & 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
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 quoteSimilar components
Browse all →
Shimmer Button
MITA dark pill button with a continuous conic-gradient light shimmer sweeping around its border.

Rainbow Gradient Button
MITA button wrapped in an animated multi-colour gradient border with a matching blurred underglow.

Interactive Hover Button
MITA pill button whose dot expands to fill the surface and reveals an arrow label on hover.

Pulsating Button
MITA solid button that emits a soft, continuously pulsing glow ring to draw the eye to the primary action.

Shiny Button
MITA frosted-glass button with a spring-animated light sweeping across its text and border.

Solid Set Button
OriginalA set of solid buttons: primary, secondary, success and danger, in three sizes.

Outline Set Button
OriginalOutline button variants across colours and sizes with a hover fill.

Ghost Set Button
OriginalGhost and text buttons with subtle hover backgrounds.

Soft Set Button
OriginalSoft, tinted buttons across semantic colours.

Gradient Set Button
OriginalGradient-filled buttons with a hover shift.
Icon Leading Button
OriginalButtons with leading and trailing icons.
Icon Only Button
OriginalSquare and circular icon-only buttons with tooltips and aria-labels.

