Icon Only Button
Original · freeSquare and circular icon-only buttons with tooltips and aria-labels.
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-icon-only.json"use client";
import { useState, useRef, useEffect, useId, type ReactNode } from "react";
import { useReducedMotion } from "motion/react";
type Shape = "square" | "circle";
type Size = "sm" | "md" | "lg";
const SIZE_MAP: Record<Size, { box: string; icon: string; radius: string }> = {
sm: { box: "h-8 w-8", icon: "h-4 w-4", radius: "rounded-lg" },
md: { box: "h-10 w-10", icon: "h-[1.15rem] w-[1.15rem]", radius: "rounded-xl" },
lg: { box: "h-12 w-12", icon: "h-5 w-5", radius: "rounded-2xl" },
};
type IconProps = { className?: string };
function IconHeart({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className={className}>
<path d="M12 21s-7.5-4.35-10.05-8.4C.36 9.98.9 6.9 3.3 5.55c1.86-1.05 4.2-.6 5.55.99L12 9.9l3.15-3.36c1.35-1.59 3.69-2.04 5.55-.99 2.4 1.35 2.94 4.43 1.35 7.05C19.5 16.65 12 21 12 21z" />
</svg>
);
}
function IconHeartOutline({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
<path d="M12 20.2s-6.7-3.9-9-7.5C1.5 10.4 2 7.7 4.1 6.5c1.7-.95 3.8-.55 5 .9L12 10.4l2.9-3c1.2-1.45 3.3-1.85 5-.9 2.1 1.2 2.6 3.9 1.1 6.2-2.3 3.6-9 7.5-9 7.5z" />
</svg>
);
}
function IconBookmark({ className, filled }: IconProps & { filled: boolean }) {
return (
<svg viewBox="0 0 24 24" fill={filled ? "currentColor" : "none"} stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
<path d="M6 4h12a1 1 0 0 1 1 1v15l-7-4-7 4V5a1 1 0 0 1 1-1z" />
</svg>
);
}
function IconCopy({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
<rect x="9" y="9" width="11" height="11" rx="2" />
<path d="M5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1" />
</svg>
);
}
function IconCheck({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
<path d="M20 6 9 17l-5-5" />
</svg>
);
}
function IconSun({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
</svg>
);
}
function IconMoon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
</svg>
);
}
function IconMute({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
<path d="M11 5 6 9H3v6h3l5 4z" />
<path d="M22 9l-6 6M16 9l6 6" />
</svg>
);
}
function IconVolume({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
<path d="M11 5 6 9H3v6h3l5 4z" />
<path d="M16 8.5a5 5 0 0 1 0 7M19 6a9 9 0 0 1 0 12" />
</svg>
);
}
function IconSettings({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
</svg>
);
}
function IconTrash({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
<path d="M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6M10 11v6M14 11v6" />
</svg>
);
}
function IconSpinner({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2} strokeLinecap="round" aria-hidden="true" className={className}>
<path d="M12 3a9 9 0 1 0 9 9" />
</svg>
);
}
function IconGithub({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className={className}>
<path d="M12 2C6.48 2 2 6.58 2 12.25c0 4.53 2.87 8.37 6.85 9.73.5.1.68-.22.68-.49 0-.24-.01-.87-.01-1.71-2.78.62-3.37-1.37-3.37-1.37-.46-1.18-1.11-1.49-1.11-1.49-.91-.63.07-.62.07-.62 1 .07 1.53 1.05 1.53 1.05.89 1.56 2.34 1.11 2.91.85.09-.66.35-1.11.63-1.36-2.22-.26-4.56-1.14-4.56-5.05 0-1.12.39-2.03 1.03-2.75-.1-.26-.45-1.3.1-2.71 0 0 .84-.27 2.75 1.05A9.35 9.35 0 0 1 12 6.84c.85 0 1.71.12 2.51.34 1.91-1.32 2.75-1.05 2.75-1.05.55 1.41.2 2.45.1 2.71.64.72 1.03 1.63 1.03 2.75 0 3.92-2.34 4.78-4.57 5.03.36.32.68.94.68 1.9 0 1.37-.01 2.48-.01 2.82 0 .27.18.6.69.49A10.02 10.02 0 0 0 22 12.25C22 6.58 17.52 2 12 2z" />
</svg>
);
}
function IconX({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className={className}>
<path d="M18.24 2.25h3.31l-7.23 8.26 8.5 11.24h-6.66l-5.22-6.82-5.97 6.82H1.66l7.73-8.84L1.24 2.25h6.83l4.71 6.23 5.46-6.23zm-1.16 17.52h1.83L7.01 4.13H5.05l12.03 15.64z" />
</svg>
);
}
function IconLinkedin({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className={className}>
<path d="M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.14 1.45-2.14 2.94v5.67H9.35V9h3.41v1.56h.05c.48-.9 1.63-1.85 3.36-1.85 3.59 0 4.25 2.36 4.25 5.44v6.3zM5.34 7.43a2.06 2.06 0 1 1 0-4.13 2.06 2.06 0 0 1 0 4.13zM7.12 20.45H3.56V9h3.56v11.45zM22.22 0H1.77C.79 0 0 .77 0 1.72v20.56C0 23.23.79 24 1.77 24h20.45c.98 0 1.78-.77 1.78-1.72V1.72C24 .77 23.2 0 22.22 0z" />
</svg>
);
}
type Tone = "neutral" | "indigo" | "rose" | "emerald";
const TONE_RING: Record<Tone, string> = {
neutral: "focus-visible:ring-slate-400 dark:focus-visible:ring-slate-500",
indigo: "focus-visible:ring-indigo-500",
rose: "focus-visible:ring-rose-500",
emerald: "focus-visible:ring-emerald-500",
};
function baseClasses(shape: Shape, size: Size) {
const s = SIZE_MAP[size];
const radius = shape === "circle" ? "rounded-full" : s.radius;
return `group relative inline-flex ${s.box} items-center justify-center ${radius} select-none outline-none transition-[transform,background-color,border-color,box-shadow,color] duration-200 ease-out focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950 active:translate-y-px disabled:pointer-events-none disabled:opacity-40`;
}
type TooltipButtonProps = {
label: string;
shape: Shape;
size: Size;
tone?: Tone;
variant?: "solid" | "soft" | "ghost" | "outline";
onClick?: () => void;
disabled?: boolean;
pressed?: boolean;
busy?: boolean;
children: ReactNode;
};
function TooltipButton({
label,
shape,
size,
tone = "neutral",
variant = "soft",
onClick,
disabled,
pressed,
busy,
children,
}: TooltipButtonProps) {
const tipId = useId();
const variantClasses: Record<string, string> = {
solid:
"bg-slate-900 text-white shadow-sm hover:bg-slate-700 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-white",
soft:
"bg-slate-100 text-slate-700 hover:bg-slate-200 hover:text-slate-900 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700 dark:hover:text-white",
ghost:
"text-slate-500 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white",
outline:
"border border-slate-300 bg-white text-slate-700 hover:border-slate-400 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:border-slate-600 dark:hover:bg-slate-800",
};
return (
<button
type="button"
aria-label={label}
aria-describedby={tipId}
aria-pressed={typeof pressed === "boolean" ? pressed : undefined}
aria-busy={busy || undefined}
disabled={disabled || busy}
onClick={onClick}
className={`${baseClasses(shape, size)} ${variantClasses[variant]} ${TONE_RING[tone]} hover:-translate-y-0.5`}
>
{children}
<span
role="tooltip"
id={tipId}
className="btnio-tip pointer-events-none absolute bottom-[calc(100%+0.6rem)] left-1/2 z-20 -translate-x-1/2 whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1 text-xs font-medium text-white opacity-0 shadow-lg dark:bg-slate-100 dark:text-slate-900"
>
{label}
<span className="absolute left-1/2 top-full h-2 w-2 -translate-x-1/2 -translate-y-1 rotate-45 bg-slate-900 dark:bg-slate-100" />
</span>
</button>
);
}
function Row({ label, children }: { label: string; children: ReactNode }) {
return (
<div className="flex flex-col gap-3">
<span className="text-[0.7rem] font-semibold uppercase tracking-[0.14em] text-slate-400 dark:text-slate-500">
{label}
</span>
<div className="flex flex-wrap items-center gap-3">{children}</div>
</div>
);
}
export default function BtnIconOnly() {
const reduceMotion = useReducedMotion();
const [liked, setLiked] = useState(false);
const [saved, setSaved] = useState(true);
const [muted, setMuted] = useState(false);
const [dark, setDark] = useState(false);
const [copied, setCopied] = useState(false);
const [loading, setLoading] = useState(false);
const copyTimer = useRef<number | null>(null);
const loadTimer = useRef<number | null>(null);
useEffect(() => {
return () => {
if (copyTimer.current) window.clearTimeout(copyTimer.current);
if (loadTimer.current) window.clearTimeout(loadTimer.current);
};
}, []);
async function handleCopy() {
try {
await navigator.clipboard.writeText("npm i @webinnoventix/ui");
} catch {
/* clipboard unavailable — still surface success state */
}
setCopied(true);
if (copyTimer.current) window.clearTimeout(copyTimer.current);
copyTimer.current = window.setTimeout(() => setCopied(false), 1600);
}
function handleLoad() {
if (loading) return;
setLoading(true);
if (loadTimer.current) window.clearTimeout(loadTimer.current);
loadTimer.current = window.setTimeout(() => setLoading(false), 1800);
}
const spin = reduceMotion ? "" : "btnio-spin";
return (
<section className="relative w-full bg-white px-6 py-20 text-slate-900 dark:bg-slate-950 dark:text-slate-100 sm:px-10">
<style>{`
.btnio-tip {
transform: translate(-50%, 4px);
transition: opacity .18s ease, transform .18s ease;
}
.group:hover .btnio-tip,
.group:focus-visible .btnio-tip {
opacity: 1;
transform: translate(-50%, 0);
}
@keyframes btnio-spin { to { transform: rotate(360deg); } }
.btnio-spin { animation: btnio-spin .8s linear infinite; transform-origin: 50% 50%; }
@keyframes btnio-pop {
0% { transform: scale(1); }
40% { transform: scale(1.28); }
100% { transform: scale(1); }
}
.btnio-pop { animation: btnio-pop .32s ease-out; }
@media (prefers-reduced-motion: reduce) {
.btnio-tip { transition: opacity .18s ease; transform: translate(-50%, 0); }
.btnio-spin { animation: none; }
.btnio-pop { animation: none; }
}
`}</style>
<div className="mx-auto max-w-4xl">
<header className="mb-14 max-w-xl">
<span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-xs font-medium text-slate-500 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-400">
<span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
FreeCode / Buttons
</span>
<h2 className="mt-5 text-3xl font-semibold tracking-tight sm:text-4xl">
Icon-only buttons
</h2>
<p className="mt-3 text-base leading-relaxed text-slate-500 dark:text-slate-400">
Square and circular controls with hover tooltips and proper{" "}
<code className="rounded bg-slate-100 px-1.5 py-0.5 text-[0.85em] text-slate-700 dark:bg-slate-800 dark:text-slate-300">
aria-label
</code>
s. Focus rings, pressed states and live feedback all wired up.
</p>
</header>
<div className="grid grid-cols-1 gap-10 rounded-3xl border border-slate-200 bg-slate-50/60 p-8 dark:border-slate-800 dark:bg-slate-900/40 sm:p-10 md:grid-cols-2">
<Row label="Square · variants">
<TooltipButton label="Settings" shape="square" size="md" variant="solid">
<IconSettings className={SIZE_MAP.md.icon} />
</TooltipButton>
<TooltipButton label="Copy install command" shape="square" size="md" variant="soft">
<IconCopy className={SIZE_MAP.md.icon} />
</TooltipButton>
<TooltipButton label="Open menu" shape="square" size="md" variant="outline">
<IconSettings className={SIZE_MAP.md.icon} />
</TooltipButton>
<TooltipButton label="More options" shape="square" size="md" variant="ghost">
<IconCopy className={SIZE_MAP.md.icon} />
</TooltipButton>
</Row>
<Row label="Circle · variants">
<TooltipButton label="Confirm" shape="circle" size="md" tone="emerald" variant="solid">
<IconCheck className={SIZE_MAP.md.icon} />
</TooltipButton>
<TooltipButton label="Notifications" shape="circle" size="md" variant="soft">
<IconVolume className={SIZE_MAP.md.icon} />
</TooltipButton>
<TooltipButton label="Profile" shape="circle" size="md" variant="outline">
<IconSettings className={SIZE_MAP.md.icon} />
</TooltipButton>
<TooltipButton label="Dismiss" shape="circle" size="md" variant="ghost">
<IconX className={SIZE_MAP.md.icon} />
</TooltipButton>
</Row>
<Row label="Sizes · sm / md / lg">
<TooltipButton label="Settings (small)" shape="square" size="sm" variant="soft">
<IconSettings className={SIZE_MAP.sm.icon} />
</TooltipButton>
<TooltipButton label="Settings (medium)" shape="square" size="md" variant="soft">
<IconSettings className={SIZE_MAP.md.icon} />
</TooltipButton>
<TooltipButton label="Settings (large)" shape="square" size="lg" variant="soft">
<IconSettings className={SIZE_MAP.lg.icon} />
</TooltipButton>
<span className="mx-1 h-8 w-px bg-slate-200 dark:bg-slate-700" />
<TooltipButton label="Confirm (small)" shape="circle" size="sm" tone="emerald" variant="soft">
<IconCheck className={SIZE_MAP.sm.icon} />
</TooltipButton>
<TooltipButton label="Confirm (medium)" shape="circle" size="md" tone="emerald" variant="soft">
<IconCheck className={SIZE_MAP.md.icon} />
</TooltipButton>
<TooltipButton label="Confirm (large)" shape="circle" size="lg" tone="emerald" variant="soft">
<IconCheck className={SIZE_MAP.lg.icon} />
</TooltipButton>
</Row>
<Row label="Stateful toggles">
<button
type="button"
aria-label={liked ? "Unlike" : "Like"}
aria-pressed={liked}
onClick={() => setLiked((v) => !v)}
className={`${baseClasses("circle", "md")} ${TONE_RING.rose} hover:-translate-y-0.5 ${
liked
? "bg-rose-50 text-rose-600 hover:bg-rose-100 dark:bg-rose-500/15 dark:text-rose-400 dark:hover:bg-rose-500/25"
: "bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-slate-200"
}`}
>
{liked ? (
<IconHeart className={`${SIZE_MAP.md.icon} ${reduceMotion ? "" : "btnio-pop"}`} />
) : (
<IconHeartOutline className={SIZE_MAP.md.icon} />
)}
<span
role="tooltip"
className="btnio-tip pointer-events-none absolute bottom-[calc(100%+0.6rem)] left-1/2 z-20 whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1 text-xs font-medium text-white opacity-0 shadow-lg dark:bg-slate-100 dark:text-slate-900"
>
{liked ? "Liked" : "Like"}
<span className="absolute left-1/2 top-full h-2 w-2 -translate-x-1/2 -translate-y-1 rotate-45 bg-slate-900 dark:bg-slate-100" />
</span>
</button>
<button
type="button"
aria-label={saved ? "Remove bookmark" : "Bookmark"}
aria-pressed={saved}
onClick={() => setSaved((v) => !v)}
className={`${baseClasses("square", "md")} ${TONE_RING.indigo} hover:-translate-y-0.5 ${
saved
? "bg-indigo-50 text-indigo-600 hover:bg-indigo-100 dark:bg-indigo-500/15 dark:text-indigo-400 dark:hover:bg-indigo-500/25"
: "bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-slate-200"
}`}
>
<IconBookmark className={`${SIZE_MAP.md.icon} ${saved && !reduceMotion ? "btnio-pop" : ""}`} filled={saved} />
<span
role="tooltip"
className="btnio-tip pointer-events-none absolute bottom-[calc(100%+0.6rem)] left-1/2 z-20 whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1 text-xs font-medium text-white opacity-0 shadow-lg dark:bg-slate-100 dark:text-slate-900"
>
{saved ? "Saved" : "Save"}
<span className="absolute left-1/2 top-full h-2 w-2 -translate-x-1/2 -translate-y-1 rotate-45 bg-slate-900 dark:bg-slate-100" />
</span>
</button>
<button
type="button"
aria-label={muted ? "Unmute" : "Mute"}
aria-pressed={muted}
onClick={() => setMuted((v) => !v)}
className={`${baseClasses("circle", "md")} ${TONE_RING.neutral} hover:-translate-y-0.5 ${
muted
? "bg-slate-900 text-white hover:bg-slate-700 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-white"
: "bg-slate-100 text-slate-600 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
}`}
>
{muted ? <IconMute className={SIZE_MAP.md.icon} /> : <IconVolume className={SIZE_MAP.md.icon} />}
<span
role="tooltip"
className="btnio-tip pointer-events-none absolute bottom-[calc(100%+0.6rem)] left-1/2 z-20 whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1 text-xs font-medium text-white opacity-0 shadow-lg dark:bg-slate-100 dark:text-slate-900"
>
{muted ? "Muted" : "Mute"}
<span className="absolute left-1/2 top-full h-2 w-2 -translate-x-1/2 -translate-y-1 rotate-45 bg-slate-900 dark:bg-slate-100" />
</span>
</button>
<button
type="button"
aria-label={dark ? "Switch to light theme" : "Switch to dark theme"}
aria-pressed={dark}
onClick={() => setDark((v) => !v)}
className={`${baseClasses("square", "md")} ${TONE_RING.indigo} hover:-translate-y-0.5 bg-slate-100 text-slate-600 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700`}
>
{dark ? <IconMoon className={SIZE_MAP.md.icon} /> : <IconSun className={SIZE_MAP.md.icon} />}
<span
role="tooltip"
className="btnio-tip pointer-events-none absolute bottom-[calc(100%+0.6rem)] left-1/2 z-20 whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1 text-xs font-medium text-white opacity-0 shadow-lg dark:bg-slate-100 dark:text-slate-900"
>
{dark ? "Dark" : "Light"}
<span className="absolute left-1/2 top-full h-2 w-2 -translate-x-1/2 -translate-y-1 rotate-45 bg-slate-900 dark:bg-slate-100" />
</span>
</button>
</Row>
<Row label="Feedback · copy & loading">
<button
type="button"
aria-label={copied ? "Copied to clipboard" : "Copy install command"}
onClick={handleCopy}
className={`${baseClasses("square", "md")} ${TONE_RING.emerald} hover:-translate-y-0.5 ${
copied
? "bg-emerald-50 text-emerald-600 dark:bg-emerald-500/15 dark:text-emerald-400"
: "bg-slate-100 text-slate-600 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
}`}
>
{copied ? (
<IconCheck className={`${SIZE_MAP.md.icon} ${reduceMotion ? "" : "btnio-pop"}`} />
) : (
<IconCopy className={SIZE_MAP.md.icon} />
)}
<span
role="tooltip"
aria-live="polite"
className="btnio-tip pointer-events-none absolute bottom-[calc(100%+0.6rem)] left-1/2 z-20 whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1 text-xs font-medium text-white opacity-0 shadow-lg dark:bg-slate-100 dark:text-slate-900"
>
{copied ? "Copied!" : "Copy"}
<span className="absolute left-1/2 top-full h-2 w-2 -translate-x-1/2 -translate-y-1 rotate-45 bg-slate-900 dark:bg-slate-100" />
</span>
</button>
<TooltipButton
label={loading ? "Loading" : "Sync workspace"}
shape="circle"
size="md"
tone="indigo"
variant="soft"
busy={loading}
onClick={handleLoad}
>
{loading ? (
<IconSpinner className={`${SIZE_MAP.md.icon} ${spin}`} />
) : (
<IconCheck className={SIZE_MAP.md.icon} />
)}
</TooltipButton>
<TooltipButton label="Delete item" shape="square" size="md" tone="rose" variant="ghost" onClick={() => undefined}>
<IconTrash className={`${SIZE_MAP.md.icon} text-rose-500 group-hover:text-rose-600 dark:group-hover:text-rose-400`} />
</TooltipButton>
<TooltipButton label="Unavailable" shape="square" size="md" variant="outline" disabled>
<IconSettings className={SIZE_MAP.md.icon} />
</TooltipButton>
</Row>
<Row label="Social · brand glyphs">
<a
href="https://github.com"
target="_blank"
rel="noopener"
aria-label="View on GitHub"
className={`${baseClasses("circle", "md")} ${TONE_RING.neutral} hover:-translate-y-0.5 bg-slate-100 text-slate-700 hover:bg-slate-900 hover:text-white dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-white dark:hover:text-slate-900`}
>
<IconGithub className={SIZE_MAP.md.icon} />
<span
role="tooltip"
className="btnio-tip pointer-events-none absolute bottom-[calc(100%+0.6rem)] left-1/2 z-20 whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1 text-xs font-medium text-white opacity-0 shadow-lg dark:bg-slate-100 dark:text-slate-900"
>
GitHub
<span className="absolute left-1/2 top-full h-2 w-2 -translate-x-1/2 -translate-y-1 rotate-45 bg-slate-900 dark:bg-slate-100" />
</span>
</a>
<a
href="https://x.com"
target="_blank"
rel="noopener"
aria-label="Follow on X"
className={`${baseClasses("circle", "md")} ${TONE_RING.neutral} hover:-translate-y-0.5 bg-slate-100 text-slate-700 hover:bg-slate-900 hover:text-white dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-white dark:hover:text-slate-900`}
>
<IconX className={`${SIZE_MAP.md.icon} scale-90`} />
<span
role="tooltip"
className="btnio-tip pointer-events-none absolute bottom-[calc(100%+0.6rem)] left-1/2 z-20 whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1 text-xs font-medium text-white opacity-0 shadow-lg dark:bg-slate-100 dark:text-slate-900"
>
X / Twitter
<span className="absolute left-1/2 top-full h-2 w-2 -translate-x-1/2 -translate-y-1 rotate-45 bg-slate-900 dark:bg-slate-100" />
</span>
</a>
<a
href="https://linkedin.com"
target="_blank"
rel="noopener"
aria-label="Connect on LinkedIn"
className={`${baseClasses("circle", "md")} focus-visible:ring-sky-500 hover:-translate-y-0.5 bg-slate-100 text-[#0a66c2] hover:bg-[#0a66c2] hover:text-white dark:bg-slate-800 dark:text-sky-400 dark:hover:bg-[#0a66c2] dark:hover:text-white`}
>
<IconLinkedin className={SIZE_MAP.md.icon} />
<span
role="tooltip"
className="btnio-tip pointer-events-none absolute bottom-[calc(100%+0.6rem)] left-1/2 z-20 whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1 text-xs font-medium text-white opacity-0 shadow-lg dark:bg-slate-100 dark:text-slate-900"
>
LinkedIn
<span className="absolute left-1/2 top-full h-2 w-2 -translate-x-1/2 -translate-y-1 rotate-45 bg-slate-900 dark:bg-slate-100" />
</span>
</a>
</Row>
</div>
<p className="mt-8 text-center text-xs text-slate-400 dark:text-slate-600">
Hover or focus any control to reveal its tooltip · every button carries an aria-label
</p>
</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.

Pill Group Button
OriginalA segmented pill button group with a sliding active indicator.

