Ghost Set Button
Original · freeGhost and text buttons with subtle hover backgrounds.
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-ghost-set.json"use client";
import { useState, type ReactNode } from "react";
type Tone = "slate" | "indigo" | "violet" | "emerald" | "rose" | "sky";
type Size = "sm" | "md" | "lg";
const toneMap: Record<
Tone,
{ text: string; hover: string; active: string; ring: string }
> = {
slate: {
text: "text-slate-700 dark:text-slate-200",
hover:
"hover:bg-slate-900/[0.06] dark:hover:bg-white/[0.08]",
active: "active:bg-slate-900/[0.10] dark:active:bg-white/[0.12]",
ring: "focus-visible:ring-slate-400 dark:focus-visible:ring-slate-500",
},
indigo: {
text: "text-indigo-600 dark:text-indigo-300",
hover: "hover:bg-indigo-500/[0.10] dark:hover:bg-indigo-400/[0.14]",
active: "active:bg-indigo-500/[0.16] dark:active:bg-indigo-400/[0.20]",
ring: "focus-visible:ring-indigo-400 dark:focus-visible:ring-indigo-500",
},
violet: {
text: "text-violet-600 dark:text-violet-300",
hover: "hover:bg-violet-500/[0.10] dark:hover:bg-violet-400/[0.14]",
active: "active:bg-violet-500/[0.16] dark:active:bg-violet-400/[0.20]",
ring: "focus-visible:ring-violet-400 dark:focus-visible:ring-violet-500",
},
emerald: {
text: "text-emerald-600 dark:text-emerald-300",
hover: "hover:bg-emerald-500/[0.10] dark:hover:bg-emerald-400/[0.14]",
active: "active:bg-emerald-500/[0.16] dark:active:bg-emerald-400/[0.20]",
ring: "focus-visible:ring-emerald-400 dark:focus-visible:ring-emerald-500",
},
rose: {
text: "text-rose-600 dark:text-rose-300",
hover: "hover:bg-rose-500/[0.10] dark:hover:bg-rose-400/[0.14]",
active: "active:bg-rose-500/[0.16] dark:active:bg-rose-400/[0.20]",
ring: "focus-visible:ring-rose-400 dark:focus-visible:ring-rose-500",
},
sky: {
text: "text-sky-600 dark:text-sky-300",
hover: "hover:bg-sky-500/[0.10] dark:hover:bg-sky-400/[0.14]",
active: "active:bg-sky-500/[0.16] dark:active:bg-sky-400/[0.20]",
ring: "focus-visible:ring-sky-400 dark:focus-visible:ring-sky-500",
},
};
const sizeMap: Record<Size, { pad: string; text: string; gap: string; icon: string }> = {
sm: { pad: "px-2.5 py-1.5", text: "text-[13px]", gap: "gap-1.5", icon: "h-3.5 w-3.5" },
md: { pad: "px-3.5 py-2", text: "text-sm", gap: "gap-2", icon: "h-4 w-4" },
lg: { pad: "px-5 py-2.5", text: "text-[15px]", gap: "gap-2.5", icon: "h-[18px] w-[18px]" },
};
const base =
"bgs-btn relative inline-flex select-none items-center justify-center rounded-lg font-medium " +
"outline-none transition-colors duration-150 focus-visible:ring-2 " +
"focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950 " +
"disabled:pointer-events-none disabled:opacity-45";
function GhostButton({
children,
tone = "slate",
size = "md",
leading,
trailing,
disabled,
loading,
underline,
"aria-label": ariaLabel,
onClick,
}: {
children?: ReactNode;
tone?: Tone;
size?: Size;
leading?: ReactNode;
trailing?: ReactNode;
disabled?: boolean;
loading?: boolean;
underline?: boolean;
"aria-label"?: string;
onClick?: () => void;
}) {
const t = toneMap[tone];
const s = sizeMap[size];
return (
<button
type="button"
aria-label={ariaLabel}
aria-busy={loading || undefined}
disabled={disabled || loading}
onClick={onClick}
className={[
base,
s.pad,
s.text,
s.gap,
t.text,
t.hover,
t.active,
t.ring,
underline
? "underline decoration-current/30 decoration-1 underline-offset-4 hover:decoration-current/60"
: "",
].join(" ")}
>
{loading ? (
<span
className={["bgs-spin shrink-0 rounded-full border-2 border-current border-r-transparent", s.icon].join(" ")}
aria-hidden="true"
/>
) : (
leading && <span className={["shrink-0", s.icon].join(" ")} aria-hidden="true">{leading}</span>
)}
{children && <span className="truncate">{children}</span>}
{!loading && trailing && (
<span className={["shrink-0", s.icon].join(" ")} aria-hidden="true">{trailing}</span>
)}
</button>
);
}
const IconArrow = (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-full w-full">
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
);
const IconPlus = (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-full w-full">
<path d="M12 5v14M5 12h14" />
</svg>
);
const IconDownload = (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-full w-full">
<path d="M12 3v12M7 10l5 5 5-5M5 21h14" />
</svg>
);
const IconTrash = (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-full w-full">
<path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14" />
</svg>
);
const IconHeart = (filled: boolean) => (
<svg viewBox="0 0 24 24" fill={filled ? "currentColor" : "none"} stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-full w-full">
<path d="M20.8 5.6a5.5 5.5 0 0 0-7.8 0L12 6.6l-1-1a5.5 5.5 0 1 0-7.8 7.8l1 1 7.8 7.8 7.8-7.8 1-1a5.5 5.5 0 0 0 0-7.8Z" />
</svg>
);
const IconStar = (filled: boolean) => (
<svg viewBox="0 0 24 24" fill={filled ? "currentColor" : "none"} stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-full w-full">
<path d="M12 3l2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 18.8 6.2 21.9l1.1-6.5L2.6 9.8l6.5-.9L12 3Z" />
</svg>
);
const IconCopy = (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-full w-full">
<rect x="9" y="9" width="12" height="12" rx="2" />
<path d="M5 15V5a2 2 0 0 1 2-2h10" />
</svg>
);
const IconCheck = (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" className="h-full w-full">
<path d="M5 13l4 4L19 7" />
</svg>
);
const IconMenu = (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-full w-full">
<path d="M4 7h16M4 12h16M4 17h16" />
</svg>
);
const IconChevron = (open: boolean) => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={["h-full w-full transition-transform duration-200", open ? "rotate-180" : ""].join(" ")}>
<path d="M6 9l6 6 6-6" />
</svg>
);
function Panel({ label, children }: { label: string; children: ReactNode }) {
return (
<div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900/60">
<p className="mb-4 text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-400 dark:text-slate-500">
{label}
</p>
<div className="flex flex-wrap items-center gap-2.5">{children}</div>
</div>
);
}
export default function BtnGhostSet() {
const [liked, setLiked] = useState(false);
const [starred, setStarred] = useState(true);
const [copied, setCopied] = useState(false);
const [expanded, setExpanded] = useState(false);
const [busy, setBusy] = useState(false);
const copy = async () => {
try {
await navigator.clipboard.writeText("npm i motion");
setCopied(true);
setTimeout(() => setCopied(false), 1600);
} catch {
setCopied(false);
}
};
const fakeSave = () => {
if (busy) return;
setBusy(true);
setTimeout(() => setBusy(false), 1800);
};
return (
<section className="relative w-full bg-slate-50 px-4 py-16 sm:px-6 dark:bg-slate-950">
<style>{`
@keyframes bgs-spin { to { transform: rotate(360deg); } }
.bgs-spin { animation: bgs-spin 0.7s linear infinite; }
.bgs-btn { -webkit-tap-highlight-color: transparent; }
.bgs-pop { animation: bgs-pop 0.28s cubic-bezier(0.34,1.56,0.64,1); }
@keyframes bgs-pop { 0% { transform: scale(0.6); } 60% { transform: scale(1.18); } 100% { transform: scale(1); } }
@media (prefers-reduced-motion: reduce) {
.bgs-spin { animation: none; }
.bgs-pop { animation: none; }
.bgs-btn { transition: none !important; }
}
`}</style>
<div className="mx-auto max-w-3xl">
<header className="mb-10">
<h2 className="text-2xl font-semibold tracking-tight text-slate-900 dark:text-white">
Ghost & text buttons
</h2>
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
Transparent by default, with a subtle tinted wash on hover. Low-emphasis actions that
stay quiet until you reach for them.
</p>
</header>
<div className="grid gap-5">
<Panel label="Sizes">
<GhostButton size="sm">Small</GhostButton>
<GhostButton size="md">Medium</GhostButton>
<GhostButton size="lg">Large</GhostButton>
</Panel>
<Panel label="Tones">
<GhostButton tone="slate">Slate</GhostButton>
<GhostButton tone="indigo">Indigo</GhostButton>
<GhostButton tone="violet">Violet</GhostButton>
<GhostButton tone="emerald">Emerald</GhostButton>
<GhostButton tone="sky">Sky</GhostButton>
<GhostButton tone="rose">Rose</GhostButton>
</Panel>
<Panel label="With icons">
<GhostButton tone="indigo" trailing={IconArrow}>
View changelog
</GhostButton>
<GhostButton tone="emerald" leading={IconPlus}>
New project
</GhostButton>
<GhostButton tone="slate" leading={IconDownload}>
Export CSV
</GhostButton>
<GhostButton tone="rose" leading={IconTrash}>
Delete draft
</GhostButton>
</Panel>
<Panel label="Text links (underline)">
<GhostButton tone="indigo" size="sm" underline>
Terms of service
</GhostButton>
<GhostButton tone="slate" size="sm" underline>
Privacy policy
</GhostButton>
<GhostButton tone="sky" size="sm" underline trailing={IconArrow}>
Read the docs
</GhostButton>
</Panel>
<Panel label="States">
<GhostButton tone="violet">Enabled</GhostButton>
<GhostButton tone="violet" disabled>
Disabled
</GhostButton>
<GhostButton tone="violet" loading>
Saving
</GhostButton>
<GhostButton tone="emerald" loading={busy} leading={IconDownload} onClick={fakeSave}>
{busy ? "Syncing" : "Sync now"}
</GhostButton>
</Panel>
<Panel label="Icon-only">
<button
type="button"
aria-label="Open menu"
className={[base, "h-9 w-9", toneMap.slate.text, toneMap.slate.hover, toneMap.slate.active, toneMap.slate.ring].join(" ")}
>
<span className="h-4 w-4" aria-hidden="true">{IconMenu}</span>
</button>
<button
type="button"
aria-label="Add item"
className={[base, "h-9 w-9", toneMap.indigo.text, toneMap.indigo.hover, toneMap.indigo.active, toneMap.indigo.ring].join(" ")}
>
<span className="h-4 w-4" aria-hidden="true">{IconPlus}</span>
</button>
<button
type="button"
aria-label="Download file"
className={[base, "h-9 w-9", toneMap.sky.text, toneMap.sky.hover, toneMap.sky.active, toneMap.sky.ring].join(" ")}
>
<span className="h-4 w-4" aria-hidden="true">{IconDownload}</span>
</button>
</Panel>
<Panel label="Interactive — toggles, copy, expand">
<button
type="button"
aria-pressed={liked}
aria-label={liked ? "Unlike" : "Like"}
onClick={() => setLiked((v) => !v)}
className={[
base,
sizeMap.md.pad,
sizeMap.md.text,
sizeMap.md.gap,
liked ? toneMap.rose.text : "text-slate-600 dark:text-slate-300",
toneMap.rose.hover,
toneMap.rose.active,
toneMap.rose.ring,
].join(" ")}
>
<span key={liked ? "on" : "off"} className={["h-4 w-4", liked ? "bgs-pop" : ""].join(" ")} aria-hidden="true">
{IconHeart(liked)}
</span>
{liked ? "Liked" : "Like"}
</button>
<button
type="button"
aria-pressed={starred}
aria-label={starred ? "Unstar repository" : "Star repository"}
onClick={() => setStarred((v) => !v)}
className={[
base,
sizeMap.md.pad,
sizeMap.md.text,
sizeMap.md.gap,
starred ? "text-amber-500 dark:text-amber-300" : "text-slate-600 dark:text-slate-300",
"hover:bg-amber-500/[0.10] dark:hover:bg-amber-400/[0.14]",
"active:bg-amber-500/[0.16] dark:active:bg-amber-400/[0.20]",
"focus-visible:ring-amber-400 dark:focus-visible:ring-amber-500",
].join(" ")}
>
<span key={starred ? "on" : "off"} className={["h-4 w-4", starred ? "bgs-pop" : ""].join(" ")} aria-hidden="true">
{IconStar(starred)}
</span>
{starred ? "Starred" : "Star"}
</button>
<button
type="button"
onClick={copy}
aria-label="Copy install command"
className={[
base,
sizeMap.md.pad,
sizeMap.md.text,
sizeMap.md.gap,
copied ? toneMap.emerald.text : toneMap.slate.text,
copied ? "" : toneMap.slate.hover,
copied ? "" : toneMap.slate.active,
copied ? toneMap.emerald.ring : toneMap.slate.ring,
].join(" ")}
>
<span className="h-4 w-4" aria-hidden="true">{copied ? IconCheck : IconCopy}</span>
{copied ? "Copied" : "npm i motion"}
</button>
<button
type="button"
aria-expanded={expanded}
onClick={() => setExpanded((v) => !v)}
className={[
base,
sizeMap.md.pad,
sizeMap.md.text,
sizeMap.md.gap,
toneMap.slate.text,
toneMap.slate.hover,
toneMap.slate.active,
toneMap.slate.ring,
].join(" ")}
>
Details
<span className="h-4 w-4" aria-hidden="true">{IconChevron(expanded)}</span>
</button>
</Panel>
{expanded && (
<div className="rounded-2xl border border-slate-200 bg-white px-6 py-5 text-sm leading-relaxed text-slate-600 shadow-sm dark:border-slate-800 dark:bg-slate-900/60 dark:text-slate-300">
Ghost buttons keep the interface calm. Reserve solid, high-contrast buttons for the
single primary action on a screen, and use these for everything secondary: cancel,
back, filters, row-level actions and inline text links.
</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.

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.

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

