02 / 51
All 51 in 51 Tailwind CSS Buttons
- 01Tailwind Submit Button (Form Primary)
- 02Tailwind Primary and Secondary Button Pair
- 03Tailwind Ghost / Outline Button
- 04Tailwind Icon-Only Button with Accessible Tooltip
- 05Tailwind Gradient CTA Button
- 06Tailwind Loading Spinner Button (State Transition)
- 07Tailwind Disabled Button State (Full State Reference)
- 08Tailwind Button Group Component (Segmented Radio-Like)
- 09Tailwind Add to Cart Button
- 10Tailwind Download Progress Button
- 11Tailwind Like Heart Button
- 12Tailwind Confirm Delete Button
- 13Tailwind Play Pause Button
- 14Tailwind Subscribe Confetti Button
- 15Tailwind Magnetic Mercury Ripple Button
- 16Tailwind Brutalist Glitch Button
- 17Tailwind Neon Plasma Burst Button
- 18Tailwind Clay Press Button
- 19Tailwind Editorial Ink Spread Button
- 20Tailwind Particle Shard Burst Button
- 21Tailwind Brushed Brass Plate Button
- 22Tailwind Washi Paper Crane Button
- 23Tailwind Frosted Ice Cube Button
- 24Tailwind Vintage Typewriter Key Button
- 25Tailwind Lacquered Walnut Wood Button
- 26Tailwind Pharma Gel Capsule Button
- 27Tailwind Aged Copper Coin Button
- 28Tailwind Origami Fortune Cookie Button
- 29Tailwind Holographic Foil Sticker Button
- 30Tailwind Brutalist Concrete Slab Button
- 31Tailwind Mercury Bead Morph Button
- 32Tailwind Soft Pill Modern SaaS Button
- 33Tailwind Masking Tape Label Button
- 34Tailwind Obsidian Glass Shard Button
- 35Tailwind Wine Velvet Ribbon Button
- 36Tailwind LED Segment Display Button
- 37Tailwind Cassette Mixtape Label Button
- 38Tailwind Chalkboard Menu Button
- 39Tailwind Vinyl Record Sleeve Ripple Button
- 40Tailwind Polaroid Snapshot Button
- 41Tailwind Airline Boarding Pass Button
- 42Tailwind Aged Brass Subway Token Button
- 43Tailwind Vintage Rx Pharmacy Label Button
- 44Tailwind Bakelite Radio Dial Toggle Button
- 45Tailwind Regal Postage Stamp Button
- 46Tailwind Victorian Cigar Band Foil Button
- 47Tailwind Vintage Calculator Key Button
- 48Tailwind Toolbox Latch Spotlight Button
- 49Tailwind Receipt Paper Total Count-Up Button
- 50Tailwind IBM Punch Card Magnetic Follow Button
- 51Tailwind Brass Periscope Lens Button
Tailwind Primary and Secondary Button Pair
One shared recipe, two visual weights, and a container that stacks them when the slot is narrow — not when the viewport is. The port is the honest test of "utilities can't do variants": they can, and the shared part stays literally identical because both buttons carry the same twenty classes and differ in four.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<div class="tb-02 w-full min-h-svh grid place-items-center bg-tb02-bg text-tb02-text font-tb02-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<div class="@container w-[min(100%,26rem)] flex flex-wrap justify-center gap-3">
<button type="button" class="cursor-pointer grid place-items-center min-h-12 px-6 border-0 rounded-xl text-[0.9375rem] font-semibold tracking-[-0.01em] transition-[translate,background-color,box-shadow] duration-200 ease-tb02-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tb02-accent disabled:cursor-not-allowed disabled:opacity-45 disabled:translate-y-0 hover:-translate-y-px active:translate-y-px motion-reduce:transition-none motion-reduce:hover:translate-y-0 @max-[22rem]:basis-full bg-tb02-accent text-tb02-on-accent hover:bg-tb02-accent-deep shadow-[0_8px_20px_-10px_var(--color-tb02-accent-glow)]">Start free trial</button>
<button type="button" class="cursor-pointer grid place-items-center min-h-12 px-6 border-0 rounded-xl text-[0.9375rem] font-semibold tracking-[-0.01em] transition-[translate,background-color,box-shadow] duration-200 ease-tb02-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tb02-accent disabled:cursor-not-allowed disabled:opacity-45 disabled:translate-y-0 hover:-translate-y-px active:translate-y-px motion-reduce:transition-none motion-reduce:hover:translate-y-0 @max-[22rem]:basis-full bg-tb02-surface text-tb02-text inset-ring-1 inset-ring-tb02-line hover:bg-tb02-tint hover:inset-ring-tb02-line-strong">Book a demo</button>
</div>
</div><div class="tb-02 w-full min-h-svh grid place-items-center bg-tb02-bg text-tb02-text font-tb02-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<div class="@container w-[min(100%,26rem)] flex flex-wrap justify-center gap-3">
<button type="button" class="cursor-pointer grid place-items-center min-h-12 px-6 border-0 rounded-xl text-[0.9375rem] font-semibold tracking-[-0.01em] transition-[translate,background-color,box-shadow] duration-200 ease-tb02-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tb02-accent disabled:cursor-not-allowed disabled:opacity-45 disabled:translate-y-0 hover:-translate-y-px active:translate-y-px motion-reduce:transition-none motion-reduce:hover:translate-y-0 @max-[22rem]:basis-full bg-tb02-accent text-tb02-on-accent hover:bg-tb02-accent-deep shadow-[0_8px_20px_-10px_var(--color-tb02-accent-glow)]">Start free trial</button>
<button type="button" class="cursor-pointer grid place-items-center min-h-12 px-6 border-0 rounded-xl text-[0.9375rem] font-semibold tracking-[-0.01em] transition-[translate,background-color,box-shadow] duration-200 ease-tb02-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tb02-accent disabled:cursor-not-allowed disabled:opacity-45 disabled:translate-y-0 hover:-translate-y-px active:translate-y-px motion-reduce:transition-none motion-reduce:hover:translate-y-0 @max-[22rem]:basis-full bg-tb02-surface text-tb02-text inset-ring-1 inset-ring-tb02-line hover:bg-tb02-tint hover:inset-ring-tb02-line-strong">Book a demo</button>
</div>
</div>@import "tailwindcss";
@theme {
--color-tb02-bg: #f6f6f4;
--color-tb02-bg: oklch(0.97 0.003 90);
--color-tb02-surface: #ffffff;
--color-tb02-tint: #f0f0ee;
--color-tb02-tint: oklch(0.95 0.003 90);
--color-tb02-text: #141416;
--color-tb02-text: oklch(0.16 0.01 285);
--color-tb02-muted: #5f5f68;
--color-tb02-muted: oklch(0.45 0.015 285);
--color-tb02-line: #dcdcd6;
--color-tb02-line: oklch(0.89 0.008 90);
--color-tb02-line-strong: #b4b4ad;
--color-tb02-line-strong: oklch(0.76 0.01 90);
--color-tb02-accent: #4f39f6;
--color-tb02-accent: oklch(0.51 0.23 279);
--color-tb02-accent-deep: #4029d6;
--color-tb02-accent-deep: oklch(0.45 0.22 279);
--color-tb02-accent-glow: #4f39f6cc;
--color-tb02-accent-glow: oklch(0.51 0.23 279 / 0.8);
--color-tb02-on-accent: #ffffff;
--font-tb02-sans: "Plus Jakarta Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb02-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-02 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
@media (prefers-color-scheme: dark) {
.tb-02:not([data-theme="light"]) {
--color-tb02-bg: oklch(0.14 0.008 285);
--color-tb02-surface: oklch(0.2 0.008 285);
--color-tb02-tint: oklch(0.25 0.008 285);
--color-tb02-text: oklch(0.96 0.004 285);
--color-tb02-line: oklch(0.32 0.01 285);
--color-tb02-line-strong: oklch(0.45 0.01 285);
--color-tb02-accent: oklch(0.7 0.16 279);
--color-tb02-on-accent: oklch(0.15 0.06 279);
}
}
.tb-02[data-theme="dark"] {
--color-tb02-bg: oklch(0.14 0.008 285);
--color-tb02-surface: oklch(0.2 0.008 285);
--color-tb02-tint: oklch(0.25 0.008 285);
--color-tb02-text: oklch(0.96 0.004 285);
--color-tb02-line: oklch(0.32 0.01 285);
--color-tb02-accent: oklch(0.7 0.16 279);
--color-tb02-on-accent: oklch(0.15 0.06 279);
}
@media (forced-colors: active) {
.tb-02 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
}
.tb-02 button:first-child {
background: Highlight;
color: HighlightText;
}
}@import "tailwindcss";
@theme {
--color-tb02-bg: #f6f6f4;
--color-tb02-bg: oklch(0.97 0.003 90);
--color-tb02-surface: #ffffff;
--color-tb02-tint: #f0f0ee;
--color-tb02-tint: oklch(0.95 0.003 90);
--color-tb02-text: #141416;
--color-tb02-text: oklch(0.16 0.01 285);
--color-tb02-muted: #5f5f68;
--color-tb02-muted: oklch(0.45 0.015 285);
--color-tb02-line: #dcdcd6;
--color-tb02-line: oklch(0.89 0.008 90);
--color-tb02-line-strong: #b4b4ad;
--color-tb02-line-strong: oklch(0.76 0.01 90);
--color-tb02-accent: #4f39f6;
--color-tb02-accent: oklch(0.51 0.23 279);
--color-tb02-accent-deep: #4029d6;
--color-tb02-accent-deep: oklch(0.45 0.22 279);
--color-tb02-accent-glow: #4f39f6cc;
--color-tb02-accent-glow: oklch(0.51 0.23 279 / 0.8);
--color-tb02-on-accent: #ffffff;
--font-tb02-sans: "Plus Jakarta Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb02-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-02 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
@media (prefers-color-scheme: dark) {
.tb-02:not([data-theme="light"]) {
--color-tb02-bg: oklch(0.14 0.008 285);
--color-tb02-surface: oklch(0.2 0.008 285);
--color-tb02-tint: oklch(0.25 0.008 285);
--color-tb02-text: oklch(0.96 0.004 285);
--color-tb02-line: oklch(0.32 0.01 285);
--color-tb02-line-strong: oklch(0.45 0.01 285);
--color-tb02-accent: oklch(0.7 0.16 279);
--color-tb02-on-accent: oklch(0.15 0.06 279);
}
}
.tb-02[data-theme="dark"] {
--color-tb02-bg: oklch(0.14 0.008 285);
--color-tb02-surface: oklch(0.2 0.008 285);
--color-tb02-tint: oklch(0.25 0.008 285);
--color-tb02-text: oklch(0.96 0.004 285);
--color-tb02-line: oklch(0.32 0.01 285);
--color-tb02-accent: oklch(0.7 0.16 279);
--color-tb02-on-accent: oklch(0.15 0.06 279);
}
@media (forced-colors: active) {
.tb-02 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
}
.tb-02 button:first-child {
background: Highlight;
color: HighlightText;
}
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tb02-bg: #f6f6f4;
--color-tb02-bg: oklch(0.97 0.003 90);
--color-tb02-surface: #ffffff;
--color-tb02-tint: #f0f0ee;
--color-tb02-tint: oklch(0.95 0.003 90);
--color-tb02-text: #141416;
--color-tb02-text: oklch(0.16 0.01 285);
--color-tb02-muted: #5f5f68;
--color-tb02-muted: oklch(0.45 0.015 285);
--color-tb02-line: #dcdcd6;
--color-tb02-line: oklch(0.89 0.008 90);
--color-tb02-line-strong: #b4b4ad;
--color-tb02-line-strong: oklch(0.76 0.01 90);
--color-tb02-accent: #4f39f6;
--color-tb02-accent: oklch(0.51 0.23 279);
--color-tb02-accent-deep: #4029d6;
--color-tb02-accent-deep: oklch(0.45 0.22 279);
--color-tb02-accent-glow: #4f39f6cc;
--color-tb02-accent-glow: oklch(0.51 0.23 279 / 0.8);
--color-tb02-on-accent: #ffffff;
--font-tb02-sans: "Plus Jakarta Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb02-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-02 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
@media (prefers-color-scheme: dark) {
.tb-02:not([data-theme="light"]) {
--color-tb02-bg: oklch(0.14 0.008 285);
--color-tb02-surface: oklch(0.2 0.008 285);
--color-tb02-tint: oklch(0.25 0.008 285);
--color-tb02-text: oklch(0.96 0.004 285);
--color-tb02-line: oklch(0.32 0.01 285);
--color-tb02-line-strong: oklch(0.45 0.01 285);
--color-tb02-accent: oklch(0.7 0.16 279);
--color-tb02-on-accent: oklch(0.15 0.06 279);
}
}
.tb-02[data-theme="dark"] {
--color-tb02-bg: oklch(0.14 0.008 285);
--color-tb02-surface: oklch(0.2 0.008 285);
--color-tb02-tint: oklch(0.25 0.008 285);
--color-tb02-text: oklch(0.96 0.004 285);
--color-tb02-line: oklch(0.32 0.01 285);
--color-tb02-accent: oklch(0.7 0.16 279);
--color-tb02-on-accent: oklch(0.15 0.06 279);
}
@media (forced-colors: active) {
.tb-02 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
}
.tb-02 button:first-child {
background: Highlight;
color: HighlightText;
}
}@import "tailwindcss";
@theme {
--color-tb02-bg: #f6f6f4;
--color-tb02-bg: oklch(0.97 0.003 90);
--color-tb02-surface: #ffffff;
--color-tb02-tint: #f0f0ee;
--color-tb02-tint: oklch(0.95 0.003 90);
--color-tb02-text: #141416;
--color-tb02-text: oklch(0.16 0.01 285);
--color-tb02-muted: #5f5f68;
--color-tb02-muted: oklch(0.45 0.015 285);
--color-tb02-line: #dcdcd6;
--color-tb02-line: oklch(0.89 0.008 90);
--color-tb02-line-strong: #b4b4ad;
--color-tb02-line-strong: oklch(0.76 0.01 90);
--color-tb02-accent: #4f39f6;
--color-tb02-accent: oklch(0.51 0.23 279);
--color-tb02-accent-deep: #4029d6;
--color-tb02-accent-deep: oklch(0.45 0.22 279);
--color-tb02-accent-glow: #4f39f6cc;
--color-tb02-accent-glow: oklch(0.51 0.23 279 / 0.8);
--color-tb02-on-accent: #ffffff;
--font-tb02-sans: "Plus Jakarta Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb02-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-02 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
@media (prefers-color-scheme: dark) {
.tb-02:not([data-theme="light"]) {
--color-tb02-bg: oklch(0.14 0.008 285);
--color-tb02-surface: oklch(0.2 0.008 285);
--color-tb02-tint: oklch(0.25 0.008 285);
--color-tb02-text: oklch(0.96 0.004 285);
--color-tb02-line: oklch(0.32 0.01 285);
--color-tb02-line-strong: oklch(0.45 0.01 285);
--color-tb02-accent: oklch(0.7 0.16 279);
--color-tb02-on-accent: oklch(0.15 0.06 279);
}
}
.tb-02[data-theme="dark"] {
--color-tb02-bg: oklch(0.14 0.008 285);
--color-tb02-surface: oklch(0.2 0.008 285);
--color-tb02-tint: oklch(0.25 0.008 285);
--color-tb02-text: oklch(0.96 0.004 285);
--color-tb02-line: oklch(0.32 0.01 285);
--color-tb02-accent: oklch(0.7 0.16 279);
--color-tb02-on-accent: oklch(0.15 0.06 279);
}
@media (forced-colors: active) {
.tb-02 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
}
.tb-02 button:first-child {
background: Highlight;
color: HighlightText;
}
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Utilities: everything structural — the container query, both variants, all four states. CSS: only the dark-scheme token block and
forced-colors. - The
color-mix()hover derivations became explicit@themetokens (--color-tb02-accent-deep,--color-tb02-tint,--color-tb02-line-strong). That is a real gain: the hover colours are now nameable, inspectable and reusable asbg-*/ring-*utilities instead of being computed inline in one rule. inset-ring-1is the v4 utility that makes the "no border on either variant" trick a one-word statement. In the CSS original it needed aninset 0 0 0 var(--ring-w)box-shadow and a paired--ring-wtoken.- The base recipe is repeated verbatim on both buttons. That is the intended trade: duplication the editor can see, rather than an indirection it cannot.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Primary and Secondary Button Pair
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-primary-and-secondary-button-pair/
One shared recipe, two visual weights, and a container that stacks them when the slot is narrow — not when the viewport is. The port is the honest test of "utilities can't do variants": they can, and the shared part stays literally identical because both buttons carry the same twenty classes and differ in four.
## HTML
```html
<div class="tb-02 w-full min-h-svh grid place-items-center bg-tb02-bg text-tb02-text font-tb02-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<div class="@container w-[min(100%,26rem)] flex flex-wrap justify-center gap-3">
<button type="button" class="cursor-pointer grid place-items-center min-h-12 px-6 border-0 rounded-xl text-[0.9375rem] font-semibold tracking-[-0.01em] transition-[translate,background-color,box-shadow] duration-200 ease-tb02-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tb02-accent disabled:cursor-not-allowed disabled:opacity-45 disabled:translate-y-0 hover:-translate-y-px active:translate-y-px motion-reduce:transition-none motion-reduce:hover:translate-y-0 @max-[22rem]:basis-full bg-tb02-accent text-tb02-on-accent hover:bg-tb02-accent-deep shadow-[0_8px_20px_-10px_var(--color-tb02-accent-glow)]">Start free trial</button>
<button type="button" class="cursor-pointer grid place-items-center min-h-12 px-6 border-0 rounded-xl text-[0.9375rem] font-semibold tracking-[-0.01em] transition-[translate,background-color,box-shadow] duration-200 ease-tb02-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tb02-accent disabled:cursor-not-allowed disabled:opacity-45 disabled:translate-y-0 hover:-translate-y-px active:translate-y-px motion-reduce:transition-none motion-reduce:hover:translate-y-0 @max-[22rem]:basis-full bg-tb02-surface text-tb02-text inset-ring-1 inset-ring-tb02-line hover:bg-tb02-tint hover:inset-ring-tb02-line-strong">Book a demo</button>
</div>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tb02-bg: #f6f6f4;
--color-tb02-bg: oklch(0.97 0.003 90);
--color-tb02-surface: #ffffff;
--color-tb02-tint: #f0f0ee;
--color-tb02-tint: oklch(0.95 0.003 90);
--color-tb02-text: #141416;
--color-tb02-text: oklch(0.16 0.01 285);
--color-tb02-muted: #5f5f68;
--color-tb02-muted: oklch(0.45 0.015 285);
--color-tb02-line: #dcdcd6;
--color-tb02-line: oklch(0.89 0.008 90);
--color-tb02-line-strong: #b4b4ad;
--color-tb02-line-strong: oklch(0.76 0.01 90);
--color-tb02-accent: #4f39f6;
--color-tb02-accent: oklch(0.51 0.23 279);
--color-tb02-accent-deep: #4029d6;
--color-tb02-accent-deep: oklch(0.45 0.22 279);
--color-tb02-accent-glow: #4f39f6cc;
--color-tb02-accent-glow: oklch(0.51 0.23 279 / 0.8);
--color-tb02-on-accent: #ffffff;
--font-tb02-sans: "Plus Jakarta Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb02-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-02 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
@media (prefers-color-scheme: dark) {
.tb-02:not([data-theme="light"]) {
--color-tb02-bg: oklch(0.14 0.008 285);
--color-tb02-surface: oklch(0.2 0.008 285);
--color-tb02-tint: oklch(0.25 0.008 285);
--color-tb02-text: oklch(0.96 0.004 285);
--color-tb02-line: oklch(0.32 0.01 285);
--color-tb02-line-strong: oklch(0.45 0.01 285);
--color-tb02-accent: oklch(0.7 0.16 279);
--color-tb02-on-accent: oklch(0.15 0.06 279);
}
}
.tb-02[data-theme="dark"] {
--color-tb02-bg: oklch(0.14 0.008 285);
--color-tb02-surface: oklch(0.2 0.008 285);
--color-tb02-tint: oklch(0.25 0.008 285);
--color-tb02-text: oklch(0.96 0.004 285);
--color-tb02-line: oklch(0.32 0.01 285);
--color-tb02-accent: oklch(0.7 0.16 279);
--color-tb02-on-accent: oklch(0.15 0.06 279);
}
@media (forced-colors: active) {
.tb-02 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
}
.tb-02 button:first-child {
background: Highlight;
color: HighlightText;
}
}
```Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Primary and Secondary Button Pair
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-primary-and-secondary-button-pair/
One shared recipe, two visual weights, and a container that stacks them when the slot is narrow — not when the viewport is. The port is the honest test of "utilities can't do variants": they can, and the shared part stays literally identical because both buttons carry the same twenty classes and differ in four.
## HTML
```html
<div class="tb-02 w-full min-h-svh grid place-items-center bg-tb02-bg text-tb02-text font-tb02-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<div class="@container w-[min(100%,26rem)] flex flex-wrap justify-center gap-3">
<button type="button" class="cursor-pointer grid place-items-center min-h-12 px-6 border-0 rounded-xl text-[0.9375rem] font-semibold tracking-[-0.01em] transition-[translate,background-color,box-shadow] duration-200 ease-tb02-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tb02-accent disabled:cursor-not-allowed disabled:opacity-45 disabled:translate-y-0 hover:-translate-y-px active:translate-y-px motion-reduce:transition-none motion-reduce:hover:translate-y-0 @max-[22rem]:basis-full bg-tb02-accent text-tb02-on-accent hover:bg-tb02-accent-deep shadow-[0_8px_20px_-10px_var(--color-tb02-accent-glow)]">Start free trial</button>
<button type="button" class="cursor-pointer grid place-items-center min-h-12 px-6 border-0 rounded-xl text-[0.9375rem] font-semibold tracking-[-0.01em] transition-[translate,background-color,box-shadow] duration-200 ease-tb02-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tb02-accent disabled:cursor-not-allowed disabled:opacity-45 disabled:translate-y-0 hover:-translate-y-px active:translate-y-px motion-reduce:transition-none motion-reduce:hover:translate-y-0 @max-[22rem]:basis-full bg-tb02-surface text-tb02-text inset-ring-1 inset-ring-tb02-line hover:bg-tb02-tint hover:inset-ring-tb02-line-strong">Book a demo</button>
</div>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tb02-bg: #f6f6f4;
--color-tb02-bg: oklch(0.97 0.003 90);
--color-tb02-surface: #ffffff;
--color-tb02-tint: #f0f0ee;
--color-tb02-tint: oklch(0.95 0.003 90);
--color-tb02-text: #141416;
--color-tb02-text: oklch(0.16 0.01 285);
--color-tb02-muted: #5f5f68;
--color-tb02-muted: oklch(0.45 0.015 285);
--color-tb02-line: #dcdcd6;
--color-tb02-line: oklch(0.89 0.008 90);
--color-tb02-line-strong: #b4b4ad;
--color-tb02-line-strong: oklch(0.76 0.01 90);
--color-tb02-accent: #4f39f6;
--color-tb02-accent: oklch(0.51 0.23 279);
--color-tb02-accent-deep: #4029d6;
--color-tb02-accent-deep: oklch(0.45 0.22 279);
--color-tb02-accent-glow: #4f39f6cc;
--color-tb02-accent-glow: oklch(0.51 0.23 279 / 0.8);
--color-tb02-on-accent: #ffffff;
--font-tb02-sans: "Plus Jakarta Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb02-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-02 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
@media (prefers-color-scheme: dark) {
.tb-02:not([data-theme="light"]) {
--color-tb02-bg: oklch(0.14 0.008 285);
--color-tb02-surface: oklch(0.2 0.008 285);
--color-tb02-tint: oklch(0.25 0.008 285);
--color-tb02-text: oklch(0.96 0.004 285);
--color-tb02-line: oklch(0.32 0.01 285);
--color-tb02-line-strong: oklch(0.45 0.01 285);
--color-tb02-accent: oklch(0.7 0.16 279);
--color-tb02-on-accent: oklch(0.15 0.06 279);
}
}
.tb-02[data-theme="dark"] {
--color-tb02-bg: oklch(0.14 0.008 285);
--color-tb02-surface: oklch(0.2 0.008 285);
--color-tb02-tint: oklch(0.25 0.008 285);
--color-tb02-text: oklch(0.96 0.004 285);
--color-tb02-line: oklch(0.32 0.01 285);
--color-tb02-accent: oklch(0.7 0.16 279);
--color-tb02-on-accent: oklch(0.15 0.06 279);
}
@media (forced-colors: active) {
.tb-02 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
}
.tb-02 button:first-child {
background: Highlight;
color: HighlightText;
}
}
```How this works
Variants become two short class lists, not a token indirection. The CSS version defined --fill/--ink/--ring and swapped them per variant. In Tailwind the shared recipe is the repeated base and the difference is stated plainly:
<!-- primary --> bg-tb02-accent text-tb02-on-accent shadow-[…]
<!-- secondary --> bg-tb02-surface text-tb02-text inset-ring-1 inset-ring-tb02-lineFour classes carry the entire hierarchy. Every state, size and focus behaviour is byte-identical between the two, which is the property the token indirection existed to guarantee.
The container query survives intact. @container on the wrapper and @max-[22rem]:basis-full on the buttons is the direct translation of container-type:inline-size plus @container (max-width:22rem) — and it is shorter, because the wrapper no longer needs a named breakpoint at all.
DOM order still carries importance. The primary is first in source, so keyboard and screen-reader order match visual weight; the stacked layout keeps it first too.
Make it yours
- A third, quieter weight is one class list:
bg-transparent text-tb02-muted hover:bg-tb02-tint. - Move the stacking point by editing
@max-[22rem]:— measured on the wrapper, so the pair behaves correctly in a sidebar and in a hero. - Destructive pair: point
--color-tb02-accentat your danger colour; the glow shadow re-derives from the same token. - Match your inputs by aligning
min-h-12androunded-xlwith your form controls. - Cancel-first platforms: add
flex-row-reverseto the wrapper, no markup change.
Gotchas — read before shipping
- A secondary button must still pass 4.5:1 on its label. Grey-on-grey is the most common failure in button pairs — test the text, not the border.
@containercreates a containment context; absolutely-positioned children inside the wrapper now resolve against it.- Don't reach for
borderon only one variant.inset-ring-*is drawn inside the box, so both buttons stay exactly the same height. - Two visually equal buttons is not hierarchy. If both read as primary, users hesitate measurably longer.
gap-3on a wrapping flex row spaces both axes, so the stacked layout is already correct — margins would double up.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Container queries need Chrome 105 / Safari 16 / Firefox 110, comfortably below Tailwind v4's own floor; without them the pair still wraps via flex-wrap and simply doesn't go full-width. inset-ring-* is a v4 utility.