32 / 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 Soft Pill Modern SaaS Button
The button a 2027 SaaS product ships: a soft pill with a true 1px gradient stroke drawn by mask-composite, an inner top-light, a spring lift, a shortcut chip that dims as the arrow slides, and a focus ring that respects the radius exactly.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<div class="tb-32 w-full min-h-svh grid place-items-center font-tb32-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" class="tb-32__btn group relative cursor-pointer grid grid-flow-col items-center gap-3 min-h-13 pl-6 pr-3 border-0 rounded-full text-[0.9375rem] font-semibold tracking-[-0.012em] text-tb32-ink bg-[linear-gradient(180deg,var(--color-tb32-surface),var(--color-tb32-surface-lo))] shadow-[inset_0_1px_0_#ffffffd9,0_1px_2px_var(--color-tb32-shadow-1),0_10px_24px_-14px_var(--color-tb32-shadow-2)] transition-[translate,box-shadow,color] duration-500 ease-tb32-spring before:content-[''] before:absolute before:inset-0 before:rounded-[inherit] before:p-px before:pointer-events-none before:bg-[linear-gradient(160deg,var(--color-tb32-stroke-a),var(--color-tb32-stroke-b))] before:[mask:linear-gradient(#000_0_0)_content-box,linear-gradient(#000_0_0)] before:[mask-composite:exclude] before:transition-[background] before:duration-300 hover:-translate-y-[3px] hover:shadow-[inset_0_1px_0_#ffffffe6,0_2px_4px_var(--color-tb32-shadow-1),0_18px_34px_-16px_var(--color-tb32-accent-glow)] hover:before:bg-[linear-gradient(160deg,var(--color-tb32-accent-soft),var(--color-tb32-stroke-b))] active:translate-y-px active:shadow-[inset_0_1px_3px_var(--color-tb32-shadow-2)] focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tb32-accent disabled:cursor-not-allowed disabled:text-tb32-muted disabled:opacity-60 disabled:translate-y-0 supports-not-[mask-composite:exclude]:border supports-not-[mask-composite:exclude]:border-tb32-stroke-b supports-not-[mask-composite:exclude]:before:hidden motion-reduce:transition-none motion-reduce:hover:translate-y-0">
<span>Create workspace</span>
<kbd class="font-tb32-sans text-[0.6875rem] font-bold tracking-[0.04em] text-tb32-muted px-2 py-1 rounded-[0.45rem] bg-tb32-chip shadow-[inset_0_0_0_1px_var(--color-tb32-chip-line)] transition-[opacity,translate] duration-400 ease-tb32-out group-hover:opacity-45 group-hover:-translate-x-0.5 motion-reduce:transition-none">⌘ K</kbd>
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-[1.05rem] mr-1.5 text-tb32-accent transition-transform duration-[450ms] ease-tb32-out group-hover:translate-x-1 motion-reduce:transition-none"><path d="M5 12h13"/><path d="M12.5 6 18.5 12l-6 6"/></svg>
</button>
</div><div class="tb-32 w-full min-h-svh grid place-items-center font-tb32-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" class="tb-32__btn group relative cursor-pointer grid grid-flow-col items-center gap-3 min-h-13 pl-6 pr-3 border-0 rounded-full text-[0.9375rem] font-semibold tracking-[-0.012em] text-tb32-ink bg-[linear-gradient(180deg,var(--color-tb32-surface),var(--color-tb32-surface-lo))] shadow-[inset_0_1px_0_#ffffffd9,0_1px_2px_var(--color-tb32-shadow-1),0_10px_24px_-14px_var(--color-tb32-shadow-2)] transition-[translate,box-shadow,color] duration-500 ease-tb32-spring before:content-[''] before:absolute before:inset-0 before:rounded-[inherit] before:p-px before:pointer-events-none before:bg-[linear-gradient(160deg,var(--color-tb32-stroke-a),var(--color-tb32-stroke-b))] before:[mask:linear-gradient(#000_0_0)_content-box,linear-gradient(#000_0_0)] before:[mask-composite:exclude] before:transition-[background] before:duration-300 hover:-translate-y-[3px] hover:shadow-[inset_0_1px_0_#ffffffe6,0_2px_4px_var(--color-tb32-shadow-1),0_18px_34px_-16px_var(--color-tb32-accent-glow)] hover:before:bg-[linear-gradient(160deg,var(--color-tb32-accent-soft),var(--color-tb32-stroke-b))] active:translate-y-px active:shadow-[inset_0_1px_3px_var(--color-tb32-shadow-2)] focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tb32-accent disabled:cursor-not-allowed disabled:text-tb32-muted disabled:opacity-60 disabled:translate-y-0 supports-not-[mask-composite:exclude]:border supports-not-[mask-composite:exclude]:border-tb32-stroke-b supports-not-[mask-composite:exclude]:before:hidden motion-reduce:transition-none motion-reduce:hover:translate-y-0">
<span>Create workspace</span>
<kbd class="font-tb32-sans text-[0.6875rem] font-bold tracking-[0.04em] text-tb32-muted px-2 py-1 rounded-[0.45rem] bg-tb32-chip shadow-[inset_0_0_0_1px_var(--color-tb32-chip-line)] transition-[opacity,translate] duration-400 ease-tb32-out group-hover:opacity-45 group-hover:-translate-x-0.5 motion-reduce:transition-none">⌘ K</kbd>
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-[1.05rem] mr-1.5 text-tb32-accent transition-transform duration-[450ms] ease-tb32-out group-hover:translate-x-1 motion-reduce:transition-none"><path d="M5 12h13"/><path d="M12.5 6 18.5 12l-6 6"/></svg>
</button>
</div>@import "tailwindcss";
@theme {
--color-tb32-bg: #f7f8fa;
--color-tb32-bg: oklch(0.975 0.004 265);
--color-tb32-surface: #ffffff;
--color-tb32-surface-lo: #f2f4f8;
--color-tb32-surface-lo: oklch(0.96 0.005 265);
--color-tb32-ink: #101114;
--color-tb32-ink: oklch(0.17 0.008 265);
--color-tb32-muted: #5f6472;
--color-tb32-muted: oklch(0.47 0.015 265);
--color-tb32-accent: #3d5afe;
--color-tb32-accent: oklch(0.52 0.22 268);
--color-tb32-accent-soft: #8a9bff;
--color-tb32-accent-soft: oklch(0.72 0.13 268);
--color-tb32-accent-glow: #3d5afe73;
--color-tb32-accent-glow: oklch(0.52 0.22 268 / 0.45);
--color-tb32-stroke-a: #e3e6ee;
--color-tb32-stroke-a: oklch(0.92 0.008 265);
--color-tb32-stroke-b: #c9cede;
--color-tb32-stroke-b: oklch(0.85 0.012 265);
--color-tb32-chip: #1011140f;
--color-tb32-chip: oklch(0.17 0.008 265 / 0.06);
--color-tb32-chip-line: #10111416;
--color-tb32-chip-line: oklch(0.17 0.008 265 / 0.09);
--color-tb32-shadow-1: #10111414;
--color-tb32-shadow-1: oklch(0.17 0.008 265 / 0.08);
--color-tb32-shadow-2: #10111473;
--color-tb32-shadow-2: oklch(0.17 0.008 265 / 0.45);
--font-tb32-sans: "Manrope", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb32-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-tb32-spring: cubic-bezier(0.16, 1, 0.3, 1);
}
@supports (transition-timing-function: linear(0, 1)) {
@theme {
--ease-tb32-spring: linear(0, 0.4 7%, 0.83 15%, 1.06 23%, 1.09 28%, 1.03 37%, 1 52%, 1);
}
}
.tb-32 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(48rem 30rem at 50% -10%, color-mix(in oklab, var(--color-tb32-accent) 8%, transparent), transparent 70%), var(--color-tb32-bg);
}
/* The -webkit- mask-composite keyword differs from the standard one ('xor' vs
'exclude') and cannot ride the same arbitrary utility. */
.tb-32__btn::before {
-webkit-mask-composite: xor;
}
@media (prefers-color-scheme: dark) {
.tb-32:not([data-theme="light"]) {
--color-tb32-bg: oklch(0.13 0.008 265);
--color-tb32-surface: oklch(0.19 0.01 265);
--color-tb32-surface-lo: oklch(0.17 0.01 265);
--color-tb32-ink: oklch(0.96 0.004 265);
--color-tb32-muted: oklch(0.72 0.015 265);
--color-tb32-stroke-a: oklch(0.29 0.012 265);
--color-tb32-stroke-b: oklch(0.38 0.015 265);
--color-tb32-accent: oklch(0.7 0.15 268);
--color-tb32-chip: oklch(0.96 0.004 265 / 0.08);
--color-tb32-chip-line: oklch(0.96 0.004 265 / 0.12);
}
}
.tb-32[data-theme="dark"] {
--color-tb32-bg: oklch(0.13 0.008 265);
--color-tb32-surface: oklch(0.19 0.01 265);
--color-tb32-surface-lo: oklch(0.17 0.01 265);
--color-tb32-ink: oklch(0.96 0.004 265);
--color-tb32-muted: oklch(0.72 0.015 265);
--color-tb32-accent: oklch(0.7 0.15 268);
}
@media (forced-colors: active) {
.tb-32__btn {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
}
.tb-32__btn::before {
display: none;
}
.tb-32__btn kbd {
border: 1px solid ButtonText;
background: ButtonFace;
}
}@import "tailwindcss";
@theme {
--color-tb32-bg: #f7f8fa;
--color-tb32-bg: oklch(0.975 0.004 265);
--color-tb32-surface: #ffffff;
--color-tb32-surface-lo: #f2f4f8;
--color-tb32-surface-lo: oklch(0.96 0.005 265);
--color-tb32-ink: #101114;
--color-tb32-ink: oklch(0.17 0.008 265);
--color-tb32-muted: #5f6472;
--color-tb32-muted: oklch(0.47 0.015 265);
--color-tb32-accent: #3d5afe;
--color-tb32-accent: oklch(0.52 0.22 268);
--color-tb32-accent-soft: #8a9bff;
--color-tb32-accent-soft: oklch(0.72 0.13 268);
--color-tb32-accent-glow: #3d5afe73;
--color-tb32-accent-glow: oklch(0.52 0.22 268 / 0.45);
--color-tb32-stroke-a: #e3e6ee;
--color-tb32-stroke-a: oklch(0.92 0.008 265);
--color-tb32-stroke-b: #c9cede;
--color-tb32-stroke-b: oklch(0.85 0.012 265);
--color-tb32-chip: #1011140f;
--color-tb32-chip: oklch(0.17 0.008 265 / 0.06);
--color-tb32-chip-line: #10111416;
--color-tb32-chip-line: oklch(0.17 0.008 265 / 0.09);
--color-tb32-shadow-1: #10111414;
--color-tb32-shadow-1: oklch(0.17 0.008 265 / 0.08);
--color-tb32-shadow-2: #10111473;
--color-tb32-shadow-2: oklch(0.17 0.008 265 / 0.45);
--font-tb32-sans: "Manrope", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb32-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-tb32-spring: cubic-bezier(0.16, 1, 0.3, 1);
}
@supports (transition-timing-function: linear(0, 1)) {
@theme {
--ease-tb32-spring: linear(0, 0.4 7%, 0.83 15%, 1.06 23%, 1.09 28%, 1.03 37%, 1 52%, 1);
}
}
.tb-32 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(48rem 30rem at 50% -10%, color-mix(in oklab, var(--color-tb32-accent) 8%, transparent), transparent 70%), var(--color-tb32-bg);
}
/* The -webkit- mask-composite keyword differs from the standard one ('xor' vs
'exclude') and cannot ride the same arbitrary utility. */
.tb-32__btn::before {
-webkit-mask-composite: xor;
}
@media (prefers-color-scheme: dark) {
.tb-32:not([data-theme="light"]) {
--color-tb32-bg: oklch(0.13 0.008 265);
--color-tb32-surface: oklch(0.19 0.01 265);
--color-tb32-surface-lo: oklch(0.17 0.01 265);
--color-tb32-ink: oklch(0.96 0.004 265);
--color-tb32-muted: oklch(0.72 0.015 265);
--color-tb32-stroke-a: oklch(0.29 0.012 265);
--color-tb32-stroke-b: oklch(0.38 0.015 265);
--color-tb32-accent: oklch(0.7 0.15 268);
--color-tb32-chip: oklch(0.96 0.004 265 / 0.08);
--color-tb32-chip-line: oklch(0.96 0.004 265 / 0.12);
}
}
.tb-32[data-theme="dark"] {
--color-tb32-bg: oklch(0.13 0.008 265);
--color-tb32-surface: oklch(0.19 0.01 265);
--color-tb32-surface-lo: oklch(0.17 0.01 265);
--color-tb32-ink: oklch(0.96 0.004 265);
--color-tb32-muted: oklch(0.72 0.015 265);
--color-tb32-accent: oklch(0.7 0.15 268);
}
@media (forced-colors: active) {
.tb-32__btn {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
}
.tb-32__btn::before {
display: none;
}
.tb-32__btn kbd {
border: 1px solid ButtonText;
background: ButtonFace;
}
}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-tb32-bg: #f7f8fa;
--color-tb32-bg: oklch(0.975 0.004 265);
--color-tb32-surface: #ffffff;
--color-tb32-surface-lo: #f2f4f8;
--color-tb32-surface-lo: oklch(0.96 0.005 265);
--color-tb32-ink: #101114;
--color-tb32-ink: oklch(0.17 0.008 265);
--color-tb32-muted: #5f6472;
--color-tb32-muted: oklch(0.47 0.015 265);
--color-tb32-accent: #3d5afe;
--color-tb32-accent: oklch(0.52 0.22 268);
--color-tb32-accent-soft: #8a9bff;
--color-tb32-accent-soft: oklch(0.72 0.13 268);
--color-tb32-accent-glow: #3d5afe73;
--color-tb32-accent-glow: oklch(0.52 0.22 268 / 0.45);
--color-tb32-stroke-a: #e3e6ee;
--color-tb32-stroke-a: oklch(0.92 0.008 265);
--color-tb32-stroke-b: #c9cede;
--color-tb32-stroke-b: oklch(0.85 0.012 265);
--color-tb32-chip: #1011140f;
--color-tb32-chip: oklch(0.17 0.008 265 / 0.06);
--color-tb32-chip-line: #10111416;
--color-tb32-chip-line: oklch(0.17 0.008 265 / 0.09);
--color-tb32-shadow-1: #10111414;
--color-tb32-shadow-1: oklch(0.17 0.008 265 / 0.08);
--color-tb32-shadow-2: #10111473;
--color-tb32-shadow-2: oklch(0.17 0.008 265 / 0.45);
--font-tb32-sans: "Manrope", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb32-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-tb32-spring: cubic-bezier(0.16, 1, 0.3, 1);
}
@supports (transition-timing-function: linear(0, 1)) {
@theme {
--ease-tb32-spring: linear(0, 0.4 7%, 0.83 15%, 1.06 23%, 1.09 28%, 1.03 37%, 1 52%, 1);
}
}
.tb-32 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(48rem 30rem at 50% -10%, color-mix(in oklab, var(--color-tb32-accent) 8%, transparent), transparent 70%), var(--color-tb32-bg);
}
/* The -webkit- mask-composite keyword differs from the standard one ('xor' vs
'exclude') and cannot ride the same arbitrary utility. */
.tb-32__btn::before {
-webkit-mask-composite: xor;
}
@media (prefers-color-scheme: dark) {
.tb-32:not([data-theme="light"]) {
--color-tb32-bg: oklch(0.13 0.008 265);
--color-tb32-surface: oklch(0.19 0.01 265);
--color-tb32-surface-lo: oklch(0.17 0.01 265);
--color-tb32-ink: oklch(0.96 0.004 265);
--color-tb32-muted: oklch(0.72 0.015 265);
--color-tb32-stroke-a: oklch(0.29 0.012 265);
--color-tb32-stroke-b: oklch(0.38 0.015 265);
--color-tb32-accent: oklch(0.7 0.15 268);
--color-tb32-chip: oklch(0.96 0.004 265 / 0.08);
--color-tb32-chip-line: oklch(0.96 0.004 265 / 0.12);
}
}
.tb-32[data-theme="dark"] {
--color-tb32-bg: oklch(0.13 0.008 265);
--color-tb32-surface: oklch(0.19 0.01 265);
--color-tb32-surface-lo: oklch(0.17 0.01 265);
--color-tb32-ink: oklch(0.96 0.004 265);
--color-tb32-muted: oklch(0.72 0.015 265);
--color-tb32-accent: oklch(0.7 0.15 268);
}
@media (forced-colors: active) {
.tb-32__btn {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
}
.tb-32__btn::before {
display: none;
}
.tb-32__btn kbd {
border: 1px solid ButtonText;
background: ButtonFace;
}
}@import "tailwindcss";
@theme {
--color-tb32-bg: #f7f8fa;
--color-tb32-bg: oklch(0.975 0.004 265);
--color-tb32-surface: #ffffff;
--color-tb32-surface-lo: #f2f4f8;
--color-tb32-surface-lo: oklch(0.96 0.005 265);
--color-tb32-ink: #101114;
--color-tb32-ink: oklch(0.17 0.008 265);
--color-tb32-muted: #5f6472;
--color-tb32-muted: oklch(0.47 0.015 265);
--color-tb32-accent: #3d5afe;
--color-tb32-accent: oklch(0.52 0.22 268);
--color-tb32-accent-soft: #8a9bff;
--color-tb32-accent-soft: oklch(0.72 0.13 268);
--color-tb32-accent-glow: #3d5afe73;
--color-tb32-accent-glow: oklch(0.52 0.22 268 / 0.45);
--color-tb32-stroke-a: #e3e6ee;
--color-tb32-stroke-a: oklch(0.92 0.008 265);
--color-tb32-stroke-b: #c9cede;
--color-tb32-stroke-b: oklch(0.85 0.012 265);
--color-tb32-chip: #1011140f;
--color-tb32-chip: oklch(0.17 0.008 265 / 0.06);
--color-tb32-chip-line: #10111416;
--color-tb32-chip-line: oklch(0.17 0.008 265 / 0.09);
--color-tb32-shadow-1: #10111414;
--color-tb32-shadow-1: oklch(0.17 0.008 265 / 0.08);
--color-tb32-shadow-2: #10111473;
--color-tb32-shadow-2: oklch(0.17 0.008 265 / 0.45);
--font-tb32-sans: "Manrope", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb32-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-tb32-spring: cubic-bezier(0.16, 1, 0.3, 1);
}
@supports (transition-timing-function: linear(0, 1)) {
@theme {
--ease-tb32-spring: linear(0, 0.4 7%, 0.83 15%, 1.06 23%, 1.09 28%, 1.03 37%, 1 52%, 1);
}
}
.tb-32 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(48rem 30rem at 50% -10%, color-mix(in oklab, var(--color-tb32-accent) 8%, transparent), transparent 70%), var(--color-tb32-bg);
}
/* The -webkit- mask-composite keyword differs from the standard one ('xor' vs
'exclude') and cannot ride the same arbitrary utility. */
.tb-32__btn::before {
-webkit-mask-composite: xor;
}
@media (prefers-color-scheme: dark) {
.tb-32:not([data-theme="light"]) {
--color-tb32-bg: oklch(0.13 0.008 265);
--color-tb32-surface: oklch(0.19 0.01 265);
--color-tb32-surface-lo: oklch(0.17 0.01 265);
--color-tb32-ink: oklch(0.96 0.004 265);
--color-tb32-muted: oklch(0.72 0.015 265);
--color-tb32-stroke-a: oklch(0.29 0.012 265);
--color-tb32-stroke-b: oklch(0.38 0.015 265);
--color-tb32-accent: oklch(0.7 0.15 268);
--color-tb32-chip: oklch(0.96 0.004 265 / 0.08);
--color-tb32-chip-line: oklch(0.96 0.004 265 / 0.12);
}
}
.tb-32[data-theme="dark"] {
--color-tb32-bg: oklch(0.13 0.008 265);
--color-tb32-surface: oklch(0.19 0.01 265);
--color-tb32-surface-lo: oklch(0.17 0.01 265);
--color-tb32-ink: oklch(0.96 0.004 265);
--color-tb32-muted: oklch(0.72 0.015 265);
--color-tb32-accent: oklch(0.7 0.15 268);
}
@media (forced-colors: active) {
.tb-32__btn {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
}
.tb-32__btn::before {
display: none;
}
.tb-32__btn kbd {
border: 1px solid ButtonText;
background: ButtonFace;
}
}<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=Manrope:wght@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=Manrope:wght@500;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Utilities: the masked gradient stroke, the spring lift, three shadow states, the chip's dim-and-shift, the arrow advance, the no-support fallback border. CSS: one line —
-webkit-mask-composite: xor— plus tokens. - That one line is the honest footnote on an otherwise complete port: the prefixed keyword differs from the standard one, so it cannot ride inside the same arbitrary value. Everything else about a gradient border is now a class list.
supports-not-[mask-composite:exclude]:puts the fallback on the element. In the CSS original it was an@supports notblock two screens away, which is exactly the kind of thing that gets lost in a copy-paste.- A11y fix from the CSS original:
--muted:#6c7180on white is ≈4.2:1, and it styles both the<kbd>chip and the disabled label. The port darkens it tooklch(0.47 0.015 265)(≈5.1:1) — and note the chip also drops to 45% opacity on hover, so the resting contrast has to have headroom.
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 Soft Pill Modern SaaS Button
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-soft-pill-modern-saas-button/
The button a 2027 SaaS product ships: a soft pill with a true 1px gradient stroke drawn by mask-composite, an inner top-light, a spring lift, a shortcut chip that dims as the arrow slides, and a focus ring that respects the radius exactly.
## HTML
```html
<div class="tb-32 w-full min-h-svh grid place-items-center font-tb32-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" class="tb-32__btn group relative cursor-pointer grid grid-flow-col items-center gap-3 min-h-13 pl-6 pr-3 border-0 rounded-full text-[0.9375rem] font-semibold tracking-[-0.012em] text-tb32-ink bg-[linear-gradient(180deg,var(--color-tb32-surface),var(--color-tb32-surface-lo))] shadow-[inset_0_1px_0_#ffffffd9,0_1px_2px_var(--color-tb32-shadow-1),0_10px_24px_-14px_var(--color-tb32-shadow-2)] transition-[translate,box-shadow,color] duration-500 ease-tb32-spring before:content-[''] before:absolute before:inset-0 before:rounded-[inherit] before:p-px before:pointer-events-none before:bg-[linear-gradient(160deg,var(--color-tb32-stroke-a),var(--color-tb32-stroke-b))] before:[mask:linear-gradient(#000_0_0)_content-box,linear-gradient(#000_0_0)] before:[mask-composite:exclude] before:transition-[background] before:duration-300 hover:-translate-y-[3px] hover:shadow-[inset_0_1px_0_#ffffffe6,0_2px_4px_var(--color-tb32-shadow-1),0_18px_34px_-16px_var(--color-tb32-accent-glow)] hover:before:bg-[linear-gradient(160deg,var(--color-tb32-accent-soft),var(--color-tb32-stroke-b))] active:translate-y-px active:shadow-[inset_0_1px_3px_var(--color-tb32-shadow-2)] focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tb32-accent disabled:cursor-not-allowed disabled:text-tb32-muted disabled:opacity-60 disabled:translate-y-0 supports-not-[mask-composite:exclude]:border supports-not-[mask-composite:exclude]:border-tb32-stroke-b supports-not-[mask-composite:exclude]:before:hidden motion-reduce:transition-none motion-reduce:hover:translate-y-0">
<span>Create workspace</span>
<kbd class="font-tb32-sans text-[0.6875rem] font-bold tracking-[0.04em] text-tb32-muted px-2 py-1 rounded-[0.45rem] bg-tb32-chip shadow-[inset_0_0_0_1px_var(--color-tb32-chip-line)] transition-[opacity,translate] duration-400 ease-tb32-out group-hover:opacity-45 group-hover:-translate-x-0.5 motion-reduce:transition-none">⌘ K</kbd>
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-[1.05rem] mr-1.5 text-tb32-accent transition-transform duration-[450ms] ease-tb32-out group-hover:translate-x-1 motion-reduce:transition-none"><path d="M5 12h13"/><path d="M12.5 6 18.5 12l-6 6"/></svg>
</button>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tb32-bg: #f7f8fa;
--color-tb32-bg: oklch(0.975 0.004 265);
--color-tb32-surface: #ffffff;
--color-tb32-surface-lo: #f2f4f8;
--color-tb32-surface-lo: oklch(0.96 0.005 265);
--color-tb32-ink: #101114;
--color-tb32-ink: oklch(0.17 0.008 265);
--color-tb32-muted: #5f6472;
--color-tb32-muted: oklch(0.47 0.015 265);
--color-tb32-accent: #3d5afe;
--color-tb32-accent: oklch(0.52 0.22 268);
--color-tb32-accent-soft: #8a9bff;
--color-tb32-accent-soft: oklch(0.72 0.13 268);
--color-tb32-accent-glow: #3d5afe73;
--color-tb32-accent-glow: oklch(0.52 0.22 268 / 0.45);
--color-tb32-stroke-a: #e3e6ee;
--color-tb32-stroke-a: oklch(0.92 0.008 265);
--color-tb32-stroke-b: #c9cede;
--color-tb32-stroke-b: oklch(0.85 0.012 265);
--color-tb32-chip: #1011140f;
--color-tb32-chip: oklch(0.17 0.008 265 / 0.06);
--color-tb32-chip-line: #10111416;
--color-tb32-chip-line: oklch(0.17 0.008 265 / 0.09);
--color-tb32-shadow-1: #10111414;
--color-tb32-shadow-1: oklch(0.17 0.008 265 / 0.08);
--color-tb32-shadow-2: #10111473;
--color-tb32-shadow-2: oklch(0.17 0.008 265 / 0.45);
--font-tb32-sans: "Manrope", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb32-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-tb32-spring: cubic-bezier(0.16, 1, 0.3, 1);
}
@supports (transition-timing-function: linear(0, 1)) {
@theme {
--ease-tb32-spring: linear(0, 0.4 7%, 0.83 15%, 1.06 23%, 1.09 28%, 1.03 37%, 1 52%, 1);
}
}
.tb-32 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(48rem 30rem at 50% -10%, color-mix(in oklab, var(--color-tb32-accent) 8%, transparent), transparent 70%), var(--color-tb32-bg);
}
/* The -webkit- mask-composite keyword differs from the standard one ('xor' vs
'exclude') and cannot ride the same arbitrary utility. */
.tb-32__btn::before {
-webkit-mask-composite: xor;
}
@media (prefers-color-scheme: dark) {
.tb-32:not([data-theme="light"]) {
--color-tb32-bg: oklch(0.13 0.008 265);
--color-tb32-surface: oklch(0.19 0.01 265);
--color-tb32-surface-lo: oklch(0.17 0.01 265);
--color-tb32-ink: oklch(0.96 0.004 265);
--color-tb32-muted: oklch(0.72 0.015 265);
--color-tb32-stroke-a: oklch(0.29 0.012 265);
--color-tb32-stroke-b: oklch(0.38 0.015 265);
--color-tb32-accent: oklch(0.7 0.15 268);
--color-tb32-chip: oklch(0.96 0.004 265 / 0.08);
--color-tb32-chip-line: oklch(0.96 0.004 265 / 0.12);
}
}
.tb-32[data-theme="dark"] {
--color-tb32-bg: oklch(0.13 0.008 265);
--color-tb32-surface: oklch(0.19 0.01 265);
--color-tb32-surface-lo: oklch(0.17 0.01 265);
--color-tb32-ink: oklch(0.96 0.004 265);
--color-tb32-muted: oklch(0.72 0.015 265);
--color-tb32-accent: oklch(0.7 0.15 268);
}
@media (forced-colors: active) {
.tb-32__btn {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
}
.tb-32__btn::before {
display: none;
}
.tb-32__btn kbd {
border: 1px solid ButtonText;
background: ButtonFace;
}
}
```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 Soft Pill Modern SaaS Button
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-soft-pill-modern-saas-button/
The button a 2027 SaaS product ships: a soft pill with a true 1px gradient stroke drawn by mask-composite, an inner top-light, a spring lift, a shortcut chip that dims as the arrow slides, and a focus ring that respects the radius exactly.
## HTML
```html
<div class="tb-32 w-full min-h-svh grid place-items-center font-tb32-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" class="tb-32__btn group relative cursor-pointer grid grid-flow-col items-center gap-3 min-h-13 pl-6 pr-3 border-0 rounded-full text-[0.9375rem] font-semibold tracking-[-0.012em] text-tb32-ink bg-[linear-gradient(180deg,var(--color-tb32-surface),var(--color-tb32-surface-lo))] shadow-[inset_0_1px_0_#ffffffd9,0_1px_2px_var(--color-tb32-shadow-1),0_10px_24px_-14px_var(--color-tb32-shadow-2)] transition-[translate,box-shadow,color] duration-500 ease-tb32-spring before:content-[''] before:absolute before:inset-0 before:rounded-[inherit] before:p-px before:pointer-events-none before:bg-[linear-gradient(160deg,var(--color-tb32-stroke-a),var(--color-tb32-stroke-b))] before:[mask:linear-gradient(#000_0_0)_content-box,linear-gradient(#000_0_0)] before:[mask-composite:exclude] before:transition-[background] before:duration-300 hover:-translate-y-[3px] hover:shadow-[inset_0_1px_0_#ffffffe6,0_2px_4px_var(--color-tb32-shadow-1),0_18px_34px_-16px_var(--color-tb32-accent-glow)] hover:before:bg-[linear-gradient(160deg,var(--color-tb32-accent-soft),var(--color-tb32-stroke-b))] active:translate-y-px active:shadow-[inset_0_1px_3px_var(--color-tb32-shadow-2)] focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tb32-accent disabled:cursor-not-allowed disabled:text-tb32-muted disabled:opacity-60 disabled:translate-y-0 supports-not-[mask-composite:exclude]:border supports-not-[mask-composite:exclude]:border-tb32-stroke-b supports-not-[mask-composite:exclude]:before:hidden motion-reduce:transition-none motion-reduce:hover:translate-y-0">
<span>Create workspace</span>
<kbd class="font-tb32-sans text-[0.6875rem] font-bold tracking-[0.04em] text-tb32-muted px-2 py-1 rounded-[0.45rem] bg-tb32-chip shadow-[inset_0_0_0_1px_var(--color-tb32-chip-line)] transition-[opacity,translate] duration-400 ease-tb32-out group-hover:opacity-45 group-hover:-translate-x-0.5 motion-reduce:transition-none">⌘ K</kbd>
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-[1.05rem] mr-1.5 text-tb32-accent transition-transform duration-[450ms] ease-tb32-out group-hover:translate-x-1 motion-reduce:transition-none"><path d="M5 12h13"/><path d="M12.5 6 18.5 12l-6 6"/></svg>
</button>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tb32-bg: #f7f8fa;
--color-tb32-bg: oklch(0.975 0.004 265);
--color-tb32-surface: #ffffff;
--color-tb32-surface-lo: #f2f4f8;
--color-tb32-surface-lo: oklch(0.96 0.005 265);
--color-tb32-ink: #101114;
--color-tb32-ink: oklch(0.17 0.008 265);
--color-tb32-muted: #5f6472;
--color-tb32-muted: oklch(0.47 0.015 265);
--color-tb32-accent: #3d5afe;
--color-tb32-accent: oklch(0.52 0.22 268);
--color-tb32-accent-soft: #8a9bff;
--color-tb32-accent-soft: oklch(0.72 0.13 268);
--color-tb32-accent-glow: #3d5afe73;
--color-tb32-accent-glow: oklch(0.52 0.22 268 / 0.45);
--color-tb32-stroke-a: #e3e6ee;
--color-tb32-stroke-a: oklch(0.92 0.008 265);
--color-tb32-stroke-b: #c9cede;
--color-tb32-stroke-b: oklch(0.85 0.012 265);
--color-tb32-chip: #1011140f;
--color-tb32-chip: oklch(0.17 0.008 265 / 0.06);
--color-tb32-chip-line: #10111416;
--color-tb32-chip-line: oklch(0.17 0.008 265 / 0.09);
--color-tb32-shadow-1: #10111414;
--color-tb32-shadow-1: oklch(0.17 0.008 265 / 0.08);
--color-tb32-shadow-2: #10111473;
--color-tb32-shadow-2: oklch(0.17 0.008 265 / 0.45);
--font-tb32-sans: "Manrope", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb32-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-tb32-spring: cubic-bezier(0.16, 1, 0.3, 1);
}
@supports (transition-timing-function: linear(0, 1)) {
@theme {
--ease-tb32-spring: linear(0, 0.4 7%, 0.83 15%, 1.06 23%, 1.09 28%, 1.03 37%, 1 52%, 1);
}
}
.tb-32 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(48rem 30rem at 50% -10%, color-mix(in oklab, var(--color-tb32-accent) 8%, transparent), transparent 70%), var(--color-tb32-bg);
}
/* The -webkit- mask-composite keyword differs from the standard one ('xor' vs
'exclude') and cannot ride the same arbitrary utility. */
.tb-32__btn::before {
-webkit-mask-composite: xor;
}
@media (prefers-color-scheme: dark) {
.tb-32:not([data-theme="light"]) {
--color-tb32-bg: oklch(0.13 0.008 265);
--color-tb32-surface: oklch(0.19 0.01 265);
--color-tb32-surface-lo: oklch(0.17 0.01 265);
--color-tb32-ink: oklch(0.96 0.004 265);
--color-tb32-muted: oklch(0.72 0.015 265);
--color-tb32-stroke-a: oklch(0.29 0.012 265);
--color-tb32-stroke-b: oklch(0.38 0.015 265);
--color-tb32-accent: oklch(0.7 0.15 268);
--color-tb32-chip: oklch(0.96 0.004 265 / 0.08);
--color-tb32-chip-line: oklch(0.96 0.004 265 / 0.12);
}
}
.tb-32[data-theme="dark"] {
--color-tb32-bg: oklch(0.13 0.008 265);
--color-tb32-surface: oklch(0.19 0.01 265);
--color-tb32-surface-lo: oklch(0.17 0.01 265);
--color-tb32-ink: oklch(0.96 0.004 265);
--color-tb32-muted: oklch(0.72 0.015 265);
--color-tb32-accent: oklch(0.7 0.15 268);
}
@media (forced-colors: active) {
.tb-32__btn {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
}
.tb-32__btn::before {
display: none;
}
.tb-32__btn kbd {
border: 1px solid ButtonText;
background: ButtonFace;
}
}
```How this works
A gradient border is impossible with border-color. The stroke is a 1px-padded pseudo-element whose two mask layers cancel everywhere except the padding ring:
before:absolute before:inset-0 before:rounded-[inherit] before:p-px
before:bg-[linear-gradient(160deg,var(--stroke-a),var(--stroke-b))]
before:[mask:linear-gradient(#000_0_0)_content-box,linear-gradient(#000_0_0)]
before:[mask-composite:exclude]Long, and worth it: the alternative is two elements and a wrapper. Note that mask-composite needs the -webkit- keyword xor as well, which the CSS block supplies — the one piece the variant cannot carry.
The <kbd> is real markup, so the shortcut is discoverable by assistive tech rather than being a styled span. It dims rather than disappears, because a shortcut that vanishes on hover is a shortcut you can't learn.
Make it yours
- Stroke:
--color-tb32-stroke-a/b. A 20% lightness delta is enough to read as a bevel. - The chip is a plain inline element — swap it for a badge or a count.
- Hover travel is
hover:-translate-y-[3px]: product-grade. 6px is marketing. - Drop the
inset 0 1px 0top-light for a matte, flat surface. - The arrow's
group-hover:translate-x-1is the smallest motion in the collection. Keep it that small.
Gotchas — read before shipping
mask-compositeuses different keywords per engine:xorfor-webkit-,excludefor the standard. Both must be declared.- The stroke occupies
before:— useafter:for any other decoration. <kbd>inside a button is fine; a nested interactive element is not. Buttons must not contain buttons.- Spring overshoot on
translatecan shimmer subpixel text. Keep it under 10%. - Focus rings on pills need
outline-offset, not an inset shadow, or the radius mismatches at the ends.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 120+ | 16.4+ | 128+ | 120+ |
mask-composite needs Chrome 120 / Safari 15.4 (prefixed) / Firefox 53; the supports-not-[...] variant falls back to a flat 1px border. linear() easing needs Chrome 113 / Safari 17.2 / Firefox 112 behind the token's cubic-bezier.