Gradient Badge
Original · freegradient badges
byWeb InnoventixReact + Tailwind
badgegradientbadges
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/badge-gradient.jsonbadge-gradient.tsx
"use client";
import { useId, useRef, useState } from "react";
import type { KeyboardEvent, ReactElement } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
const CSS = `
@keyframes gbadge-flow { 0%, 100% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } }
@keyframes gbadge-shine { 0% { background-position: 150% 0; } 100% { background-position: -150% 0; } }
.gbadge-flow { animation: gbadge-flow 5s ease infinite; }
.gbadge-shine {
background-image: linear-gradient(100deg, transparent 25%, rgba(255,255,255,0.55) 50%, transparent 75%);
background-size: 220% 100%;
animation: gbadge-shine 2.6s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.gbadge-flow, .gbadge-shine { animation: none !important; }
}
`;
type Grad = { from: string; to: string; solid: string; shadow: string };
const G = {
iv: { from: "from-indigo-500", to: "to-violet-500", solid: "text-indigo-500", shadow: "shadow-indigo-500/40" },
si: { from: "from-sky-500", to: "to-indigo-500", solid: "text-sky-500", shadow: "shadow-sky-500/40" },
vr: { from: "from-violet-500", to: "to-rose-500", solid: "text-violet-500", shadow: "shadow-violet-500/40" },
se: { from: "from-sky-500", to: "to-emerald-500", solid: "text-emerald-600", shadow: "shadow-emerald-500/40" },
ar: { from: "from-amber-500", to: "to-rose-500", solid: "text-amber-600", shadow: "shadow-amber-500/40" },
es: { from: "from-emerald-500", to: "to-sky-500", solid: "text-emerald-600", shadow: "shadow-emerald-500/40" },
is: { from: "from-indigo-500", to: "to-sky-500", solid: "text-indigo-500", shadow: "shadow-indigo-500/40" },
ra: { from: "from-rose-500", to: "to-amber-500", solid: "text-rose-500", shadow: "shadow-rose-500/40" },
ev: { from: "from-emerald-500", to: "to-violet-500", solid: "text-emerald-600", shadow: "shadow-emerald-500/40" },
rv: { from: "from-rose-500", to: "to-violet-500", solid: "text-rose-500", shadow: "shadow-rose-500/40" },
} satisfies Record<string, Grad>;
const BADGES: { id: string; label: string; g: Grad }[] = [
{ id: "react", label: "React 19", g: G.iv },
{ id: "typescript", label: "TypeScript 5.6", g: G.si },
{ id: "motion", label: "Motion", g: G.vr },
{ id: "tailwind", label: "Tailwind v4", g: G.se },
{ id: "vite", label: "Vite 6", g: G.ar },
{ id: "node", label: "Node 22 LTS", g: G.es },
{ id: "wcag", label: "WCAG 2.2 AA", g: G.is },
{ id: "graphql", label: "GraphQL", g: G.ra },
{ id: "playwright", label: "Playwright", g: G.ev },
{ id: "radix", label: "Radix UI", g: G.rv },
{ id: "zod", label: "Zod", g: G.is },
{ id: "postgres", label: "PostgreSQL 17", g: G.se },
];
type Variant = "filled" | "outline" | "glow" | "animated";
const VARIANTS: { id: Variant; label: string; icon: (p: { className?: string }) => ReactElement }[] = [
{ id: "filled", label: "Filled", icon: FilledIcon },
{ id: "outline", label: "Outline", icon: OutlineIcon },
{ id: "glow", label: "Glow", icon: GlowIcon },
{ id: "animated", label: "Animated", icon: AnimatedIcon },
];
function styleFor(selected: boolean, variant: Variant, g: Grad) {
const surfaceBase = "flex items-center gap-1.5 px-3.5 py-1.5";
if (!selected) {
return {
wrap:
"rounded-full bg-slate-100 ring-1 ring-inset ring-slate-200 transition-colors hover:bg-slate-200 dark:bg-slate-800/70 dark:ring-slate-700 dark:hover:bg-slate-800",
surface: surfaceBase,
text: "text-slate-600 dark:text-slate-300",
check: "text-slate-400 dark:text-slate-500",
glow: false,
animated: false,
};
}
switch (variant) {
case "outline":
return {
wrap: `rounded-full bg-gradient-to-r p-[1.5px] transition-opacity hover:opacity-90 ${g.from} ${g.to}`,
surface: `${surfaceBase} rounded-full bg-white dark:bg-slate-950`,
text: `bg-gradient-to-r bg-clip-text font-semibold text-transparent ${g.from} ${g.to}`,
check: g.solid,
glow: false,
animated: false,
};
case "glow":
return {
wrap: "relative rounded-full",
surface: `relative z-10 rounded-full bg-gradient-to-r shadow-lg transition ${surfaceBase} ${g.from} ${g.to} ${g.shadow} hover:brightness-110`,
text: "font-semibold text-white",
check: "text-white",
glow: true,
animated: false,
};
case "animated":
return {
wrap: `relative overflow-hidden rounded-full bg-gradient-to-r bg-[length:200%_100%] transition hover:brightness-110 gbadge-flow ${g.from} ${g.to}`,
surface: `relative z-10 ${surfaceBase}`,
text: "font-semibold text-white",
check: "text-white",
glow: false,
animated: true,
};
default:
return {
wrap: `rounded-full bg-gradient-to-r transition hover:brightness-110 ${g.from} ${g.to}`,
surface: surfaceBase,
text: "font-semibold text-white",
check: "text-white",
glow: false,
animated: false,
};
}
}
export default function BadgeGradient() {
const reduce = useReducedMotion();
const uid = useId();
const [variant, setVariant] = useState<Variant>("filled");
const [selected, setSelected] = useState<string[]>(["react", "typescript", "motion", "tailwind"]);
const variantRefs = useRef<(HTMLButtonElement | null)[]>([]);
const count = selected.length;
function toggle(id: string) {
setSelected((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
}
function onVariantKey(e: KeyboardEvent<HTMLButtonElement>, i: number) {
let next = i;
if (e.key === "ArrowRight" || e.key === "ArrowDown") next = (i + 1) % VARIANTS.length;
else if (e.key === "ArrowLeft" || e.key === "ArrowUp") next = (i - 1 + VARIANTS.length) % VARIANTS.length;
else if (e.key === "Home") next = 0;
else if (e.key === "End") next = VARIANTS.length - 1;
else return;
e.preventDefault();
setVariant(VARIANTS[next].id);
variantRefs.current[next]?.focus();
}
return (
<section className="relative w-full overflow-hidden bg-white px-4 py-20 dark:bg-slate-950 sm:px-6 sm:py-28 lg:px-8">
<style>{CSS}</style>
<div aria-hidden className="pointer-events-none absolute inset-0 overflow-hidden">
<div className="absolute -right-24 -top-24 h-72 w-72 rounded-full bg-gradient-to-br from-indigo-500/20 to-violet-500/10 blur-3xl" />
<div className="absolute -bottom-24 -left-24 h-72 w-72 rounded-full bg-gradient-to-tr from-sky-500/20 to-emerald-500/10 blur-3xl" />
</div>
<div className="relative mx-auto w-full max-w-5xl">
<header className="mb-10 max-w-2xl">
<span className="inline-flex items-center gap-1.5 rounded-full bg-gradient-to-r from-indigo-500 to-violet-500 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-white">
<SparkleIcon className="h-3.5 w-3.5" />
Badges
</span>
<h2 className="mt-4 text-3xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
Gradient badges
</h2>
<p className="mt-3 text-base leading-relaxed text-slate-600 dark:text-slate-400">
Choose a treatment, then tag your stack. Every chip is a genuine toggle — keyboard-navigable,
screen-reader labelled, and rendered across four gradient styles for light and dark.
</p>
</header>
<div className="rounded-3xl border border-slate-200 bg-slate-50/60 p-6 dark:border-slate-800 dark:bg-slate-900/40 sm:p-8">
<div className="flex flex-col gap-4 border-b border-slate-200 pb-6 dark:border-slate-800 sm:flex-row sm:items-center sm:justify-between">
<div>
<p
id={`${uid}-style`}
className="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
>
Style
</p>
<div
role="radiogroup"
aria-labelledby={`${uid}-style`}
className="inline-flex rounded-full bg-slate-200/70 p-1 dark:bg-slate-800/70"
>
{VARIANTS.map((v, i) => {
const active = v.id === variant;
const Icon = v.icon;
return (
<button
key={v.id}
ref={(el) => {
variantRefs.current[i] = el;
}}
type="button"
role="radio"
aria-checked={active}
tabIndex={active ? 0 : -1}
onClick={() => setVariant(v.id)}
onKeyDown={(e) => onVariantKey(e, i)}
className={`inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-sm font-medium outline-none transition focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-100 dark:focus-visible:ring-offset-slate-900 ${
active
? "bg-white text-slate-900 shadow-sm dark:bg-slate-950 dark:text-white"
: "text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
}`}
>
<Icon className="h-4 w-4" />
{v.label}
</button>
);
})}
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 text-sm text-slate-600 dark:text-slate-300">
<span className="relative inline-flex h-6 w-6 items-center justify-center overflow-hidden rounded-md bg-gradient-to-r from-indigo-500 to-violet-500 font-semibold tabular-nums text-white">
<AnimatePresence initial={false} mode="popLayout">
<motion.span
key={count}
initial={reduce ? false : { y: "120%", opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={reduce ? { opacity: 0 } : { y: "-120%", opacity: 0 }}
transition={{ duration: 0.24, ease: "easeOut" }}
>
{count}
</motion.span>
</AnimatePresence>
</span>
selected
</div>
<div className="flex items-center gap-1 text-sm">
<button
type="button"
onClick={() => setSelected(BADGES.map((b) => b.id))}
className="rounded-md px-2 py-1 font-medium text-slate-600 outline-none transition hover:bg-slate-200 focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-slate-300 dark:hover:bg-slate-800"
>
All
</button>
<button
type="button"
onClick={() => setSelected([])}
className="rounded-md px-2 py-1 font-medium text-slate-600 outline-none transition hover:bg-slate-200 focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-slate-300 dark:hover:bg-slate-800"
>
Clear
</button>
</div>
</div>
</div>
<div role="group" aria-label="Project technologies" className="flex flex-wrap gap-2.5 pt-6 sm:gap-3">
{BADGES.map((b, i) => {
const isSel = selected.includes(b.id);
const s = styleFor(isSel, variant, b.g);
return (
<motion.button
key={b.id}
type="button"
aria-pressed={isSel}
onClick={() => toggle(b.id)}
initial={reduce ? false : { opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: reduce ? 0 : i * 0.03 }}
whileHover={reduce ? undefined : { y: -1 }}
whileTap={reduce ? undefined : { scale: 0.96 }}
className={`group relative inline-flex select-none text-sm outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:focus-visible:ring-offset-slate-900 ${s.wrap}`}
>
{s.glow && (
<span
aria-hidden
className={`pointer-events-none absolute -inset-[3px] rounded-full bg-gradient-to-r opacity-70 blur-md ${b.g.from} ${b.g.to}`}
/>
)}
{s.animated && <span aria-hidden className="gbadge-shine pointer-events-none absolute inset-0" />}
<span className={s.surface}>
<span className={s.text}>{b.label}</span>
{isSel ? (
<CheckIcon className={`h-3.5 w-3.5 ${s.check}`} />
) : (
<PlusIcon className={`h-3.5 w-3.5 ${s.check}`} />
)}
</span>
</motion.button>
);
})}
</div>
<p className="mt-6 text-xs text-slate-500 dark:text-slate-400">
Tab to a chip and press{" "}
<kbd className="rounded border border-slate-300 bg-white px-1 py-0.5 font-mono text-[10px] text-slate-700 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-300">
Space
</kbd>{" "}
to toggle. Use the arrow keys inside the style switcher.
</p>
<p aria-live="polite" className="sr-only">
{count} of {BADGES.length} technologies selected. Style: {variant}.
</p>
</div>
<div className="mt-10">
<p className="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
In context
</p>
<div className="flex flex-wrap items-center gap-2.5">
<span className="inline-flex items-center rounded-full bg-gradient-to-r from-indigo-500 to-violet-500 px-3 py-1 text-xs font-semibold text-white">
New
</span>
<span className="inline-flex items-center rounded-full bg-gradient-to-r from-sky-500 to-indigo-500 p-[1.5px] text-xs font-semibold">
<span className="rounded-full bg-white px-3 py-0.5 dark:bg-slate-950">
<span className="bg-gradient-to-r from-sky-500 to-indigo-500 bg-clip-text text-transparent">Beta</span>
</span>
</span>
<span className="inline-flex items-center gap-1.5 rounded-full bg-gradient-to-r from-emerald-500 to-sky-500 px-3 py-1 text-xs font-semibold text-white">
<span className="relative flex h-1.5 w-1.5">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-white opacity-75" />
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-white" />
</span>
Live
</span>
<span className="inline-flex items-center rounded-full bg-gradient-to-r from-amber-500 to-rose-500 px-3 py-1 text-xs font-semibold text-white shadow-lg shadow-rose-500/30">
Pro
</span>
<span className="inline-flex items-center rounded-full bg-gradient-to-r from-rose-500 to-amber-500 px-3 py-1 text-xs font-semibold text-white">
Save 40%
</span>
</div>
</div>
</div>
</section>
);
}
function FilledIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<rect x="2" y="2" width="12" height="12" rx="4" />
</svg>
);
}
function OutlineIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" aria-hidden="true">
<rect x="2.8" y="2.8" width="10.4" height="10.4" rx="3.4" />
</svg>
);
}
function GlowIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
aria-hidden="true"
>
<circle cx="8" cy="8" r="3" fill="currentColor" stroke="none" />
<path d="M8 1v1.6M8 13.4V15M1 8h1.6M13.4 8H15M3 3l1.1 1.1M11.9 11.9 13 13M13 3l-1.1 1.1M4.1 11.9 3 13" />
</svg>
);
}
function AnimatedIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M9 1 3 9h3.5L7 15l6-8H9.3L9 1z" />
</svg>
);
}
function CheckIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="m3.5 8.5 3 3 6-7" />
</svg>
);
}
function PlusIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
aria-hidden="true"
>
<path d="M8 3.5v9M3.5 8h9" />
</svg>
);
}
function SparkleIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M8 0.5 9.4 6.6 15.5 8 9.4 9.4 8 15.5 6.6 9.4 0.5 8 6.6 6.6 8 0.5z" />
</svg>
);
}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 quoteSimilar components
Browse all →
Basic Badge
Originalbasic badges across colours

Dot Badge
Originaldot status badges

Count Badge
Originalnotification count badges

Status Badge
Originalstatus pills (active/pending/error)

Pill Badge
Originalsoft pill badges

Outline Badge
Originaloutline badges
Icon Badge
Originalbadges with leading icons

Removable Badge
Originalremovable chip badges

Input Chip
Originalchip input tokens

Filter Chip
Originalselectable filter chips

Cloud Tag
Originalweighted tag cloud

Notification Badge
Originalicon with notification badge

