Tailwind Button Collection: 15 Variants for Every Use Case
15 Tailwind CSS button variants — from ghost to gradient to glassmorphism — with real TSX code, dark mode support, and Tailwind v4 class patterns for production React apps.
Why Your Button Components Are Probably Holding You Back
Honestly, most React projects ship with one button component and call it done — a blue rectangle with a hover state and maybe a disabled opacity. That's fine for a prototype. It's not fine when you're building a product that needs to communicate hierarchy, intent, and brand all at once.
Buttons are the most-clicked element on any interface. A primary CTA, a destructive delete action, a ghost navigation link — they all deserve different visual weight. Cramming everything into one btn class leads to a component that's simultaneously doing too much and not enough.
This collection covers 15 distinct button variants built with Tailwind CSS (tested on v4.0.2). Each one is a standalone TSX snippet you can drop straight into your project. No wrapper library. No extra dependencies beyond Tailwind itself.
The Base Button: Shared Utility Classes First
Before the variants, you need a shared base. This is the foundation every button in this collection inherits from. Getting this right saves you from copy-pasting px-4 py-2 rounded-md font-medium transition-all duration-150 into 15 different places.
Here's the base component in TSX. It uses Tailwind's class-variance-authority pattern if you want it, but the raw classes are shown for clarity:
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: string;
};
export const Button = ({ className, children, ...props }: ButtonProps) => (
<button
className={[
'inline-flex items-center justify-center gap-2',
'px-4 py-2 rounded-lg font-medium text-sm',
'transition-all duration-150 ease-out',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
'disabled:opacity-50 disabled:pointer-events-none',
className,
].join(' ')}
{...props}
>
{children}
</button>
);The 8px vertical padding (py-2) hits the right touch target minimum for mobile without feeling bloated on desktop. The gap-2 handles icon spacing automatically — add a Lucide icon as a sibling and it just works.
Solid, Ghost, and Outline Variants
These three are the workhorse trio. Solid for primary actions. Outline for secondary. Ghost for low-prominence actions in toolbars, nav links, or table row actions where you don't want visual clutter.
Solid: bg-blue-600 text-white hover:bg-blue-700 active:bg-blue-800 focus-visible:ring-blue-500
Outline: border border-blue-600 text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-950 focus-visible:ring-blue-500
Ghost: text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800 focus-visible:ring-slate-400
Notice the dark mode classes are explicit — no dark: toggle magic that breaks when you add a custom theme. If you're managing themes with a React context, check out the theme toggle pattern in React for a clean approach that pairs well with these variants.
Gradient, Glassmorphism, and Neon Button Variants
Now for the visual statement pieces. These are the buttons that go on landing pages, hero sections, and anywhere you need users to feel something before they click.
The gradient button uses Tailwind's bg-gradient-to-r utilities. In Tailwind v4.0.2, you can combine oklch color stops for perceptually uniform gradients — the resulting color transitions look noticeably smoother than hex-based ones. If you haven't explored oklch yet, the Tailwind oklch colors guide is worth a read.
// Gradient
<button className="bg-gradient-to-r from-violet-600 to-indigo-600 text-white px-5 py-2.5 rounded-lg font-semibold hover:from-violet-700 hover:to-indigo-700 shadow-lg shadow-indigo-500/30 transition-all duration-150">
Get Started
</button>
// Glassmorphism
<button className="bg-white/10 backdrop-blur-md border border-white/20 text-white px-5 py-2.5 rounded-lg font-medium hover:bg-white/20 transition-all duration-150">
Learn More
</button>
// Neon (dark backgrounds only)
<button className="border border-cyan-400 text-cyan-400 px-5 py-2.5 rounded-lg font-medium shadow-[0_0_12px_rgba(34,211,238,0.4)] hover:shadow-[0_0_20px_rgba(34,211,238,0.6)] transition-all duration-150">
Explore
</button>The glassmorphism variant uses rgba(255,255,255,0.10) via bg-white/10 and rgba(255,255,255,0.20) on hover. It only works on top of a non-white background — pair it with a gradient or image hero. For the full theory behind this effect, what is glassmorphism breaks it down properly.
Destructive, Success, and Warning State Variants
Semantic color buttons communicate intent without the user having to read the label. Red means danger. Green means success or confirmation. Amber means proceed with caution. These aren't arbitrary — they map to established UI conventions that users already understand.
Destructive: bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-500
Success: bg-emerald-600 text-white hover:bg-emerald-700 focus-visible:ring-emerald-500
Warning: bg-amber-500 text-white hover:bg-amber-600 focus-visible:ring-amber-400
One thing worth noting: don't rely purely on color. Add an icon or explicit label text. A red button that says "Delete" reads as destructive to sighted users and screen readers alike. A red button with no text fails half your audience. This is also where the gap-2 from the base component earns its keep — slap a trash icon next to "Delete" and it renders perfectly with zero extra CSS.
Loading, Icon-Only, and Size Variants
Loading states are where a lot of implementations fall apart. You either get a janky layout shift when the spinner appears, or the button width collapses. The fix is simple: keep the original text visible (or a placeholder with the same width) and overlay the spinner.
const LoadingButton = ({ isLoading, children, ...props }: LoadingButtonProps) => (
<button
disabled={isLoading}
className="relative bg-blue-600 text-white px-4 py-2 rounded-lg font-medium min-w-[120px] transition-all duration-150 disabled:opacity-80"
{...props}
>
<span className={isLoading ? 'opacity-0' : 'opacity-100'}>{children}</span>
{isLoading && (
<span className="absolute inset-0 flex items-center justify-center">
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
</svg>
</span>
)}
</button>
);Icon-only buttons need an explicit aria-label. No exceptions. rounded-full with equal padding (p-2) gives the circular shape. For sizes, you're looking at three Tailwind patterns: text-xs px-3 py-1.5 (sm), text-sm px-4 py-2 (md, the default), and text-base px-6 py-3 (lg). The 6px step between sizes keeps the scale consistent without jumping too aggressively.
Tailwind v4 Specific Patterns for Buttons
Tailwind v4.0.2 ships with a few things that change how you might write button styles. The @apply directive still works but the recommended pattern is moving toward native CSS cascade layers with @layer components. You can now use arbitrary properties inline without the [] escape hatch in most cases.
The bigger change is first-party support for color-mix() in the color system. This means you can do hover:bg-blue-600/90 with full browser support in v4, where previously you'd need to write hover:bg-[rgba(37,99,235,0.9)]. It's a small thing but it cleans up class strings noticeably. The Tailwind v4 features breakdown covers the full list of changes if you're upgrading an existing project.
Container queries are also worth mentioning here. If your button component lives inside a sidebar that resizes, you might want it to adapt its padding or hide its label. The container queries guide has the exact pattern for this. Short version: @container on the parent, @sm:px-4 or @sm:hidden on the button children.
Putting It All Together: A Button Gallery Component
How do you actually showcase all 15 variants during development? You build a simple gallery page. This is also useful for design reviews — just share the URL and your designer can see every variant in context without running the full app.
The gallery itself is just a grid. grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-5 across a dark and light background section. Each button renders with its variant name below it in text-xs text-slate-400 text-center mt-2.
Why does this matter? Because buttons that look great in isolation sometimes clash when placed next to each other. The gap between a ghost and a solid button in a button group shouldn't be more than 8px — use gap-2 in a flex container, not margins. And if you're combining these with animated background effects, particles background in React has patterns for making sure the z-index stacking doesn't swallow your button click events.
Ultimately, a button is never just a button. It's the most common decision point in your interface. Spending time on these 15 variants upfront means you're not making ad-hoc styling decisions at 11pm before a launch.
FAQ
Yes. The class strings are pure Tailwind CSS — copy them into plain HTML and they work. The TSX wrappers in the examples are optional convenience. If you're using just HTML, replace className with class and drop the TypeScript types.
Add dark mode classes explicitly per variant rather than relying on a single CSS variable. For solid variants, dark mode usually means slightly lighter backgrounds (blue-500 instead of blue-600). For ghost and outline, flip the hover background from slate-100 to slate-800. Using Tailwind's dark: prefix with a class-based strategy (darkMode: 'class' in your config) gives you the most control.
Use aria-disabled='true' on the element plus pointer-events-none opacity-50 classes. This keeps the element in the tab order (so screen readers announce it) while preventing interaction. The native disabled attribute removes it from focus entirely, which can confuse keyboard users who don't understand why a button they tabbed to isn't responding.
Glassmorphism needs contrast beneath it to work. The effect is bg-white/10 over a colored or image background — on white, 10% white is invisible. Either add a gradient or solid colored background behind it, or switch to a different variant for light backgrounds. The glassmorphism effect is specifically designed for dark or colorful hero sections.
Set a min-width on the button (min-w-[120px] works for most labels) and use absolute positioning for the spinner. Keep the label text in the DOM but set opacity-0 on it when loading — this preserves the button's dimensions while hiding the text. Avoid using conditional rendering (unmounting the text) as it causes the width to collapse.
Yes, the class names are identical between v3 and v4. The main thing to watch is that v4 removed the need for tailwind.config.js for most use cases — if you're using @theme in your CSS file instead, your custom color tokens will still work with these variants as long as you reference them with the correct syntax (var(--color-brand-500) or the utility class Tailwind generates from them).