08 / 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 Button Group Component (Segmented Radio-Like)
A segmented control on real radio inputs, so arrow-key navigation, roving focus and form serialisation come from the platform. The sliding thumb tracks the checked segment, and the port makes the label's own inversion a has-checked: variant — the state is read where it is rendered, not from four rules at the bottom of a stylesheet.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<div class="tb-08 w-full min-h-svh grid place-items-center bg-tb08-bg font-tb08-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<div role="group" aria-label="Chart range" class="tb-08__group relative flex w-[min(100%,25rem)] p-[0.3rem] rounded-[1rem] bg-tb08-track shadow-[inset_0_1px_2px_var(--color-tb08-inset)] active:[&_.tb-08\_\_thumb]:scale-[0.97]">
<span aria-hidden="true" class="tb-08__thumb absolute z-0 top-[0.3rem] left-[0.3rem] bottom-[0.3rem] w-[calc((100%-0.6rem)/4)] rounded-[0.7rem] bg-tb08-thumb shadow-[0_1px_2px_var(--color-tb08-shadow-1),0_6px_14px_-8px_var(--color-tb08-shadow-2)] transition-[translate,scale] duration-500 ease-tb08-spring motion-reduce:transition-none"></span>
<label class="relative z-10 flex-1 cursor-pointer grid place-items-center min-h-11 px-2 rounded-[0.7rem] text-sm font-semibold tracking-[-0.005em] text-tb08-muted transition-colors duration-200 [-webkit-tap-highlight-color:transparent] hover:text-tb08-ink-soft has-checked:text-tb08-ink has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-tb08-accent motion-reduce:transition-none"><input type="radio" name="tb-08-range" value="day" class="sr-only"><span>Day</span></label>
<label class="relative z-10 flex-1 cursor-pointer grid place-items-center min-h-11 px-2 rounded-[0.7rem] text-sm font-semibold tracking-[-0.005em] text-tb08-muted transition-colors duration-200 [-webkit-tap-highlight-color:transparent] hover:text-tb08-ink-soft has-checked:text-tb08-ink has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-tb08-accent motion-reduce:transition-none"><input type="radio" name="tb-08-range" value="week" checked class="sr-only"><span>Week</span></label>
<label class="relative z-10 flex-1 cursor-pointer grid place-items-center min-h-11 px-2 rounded-[0.7rem] text-sm font-semibold tracking-[-0.005em] text-tb08-muted transition-colors duration-200 [-webkit-tap-highlight-color:transparent] hover:text-tb08-ink-soft has-checked:text-tb08-ink has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-tb08-accent motion-reduce:transition-none"><input type="radio" name="tb-08-range" value="month" class="sr-only"><span>Month</span></label>
<label class="relative z-10 flex-1 cursor-pointer grid place-items-center min-h-11 px-2 rounded-[0.7rem] text-sm font-semibold tracking-[-0.005em] text-tb08-muted transition-colors duration-200 [-webkit-tap-highlight-color:transparent] hover:text-tb08-ink-soft has-checked:text-tb08-ink has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-tb08-accent motion-reduce:transition-none"><input type="radio" name="tb-08-range" value="year" class="sr-only"><span>Year</span></label>
</div>
</div><div class="tb-08 w-full min-h-svh grid place-items-center bg-tb08-bg font-tb08-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<div role="group" aria-label="Chart range" class="tb-08__group relative flex w-[min(100%,25rem)] p-[0.3rem] rounded-[1rem] bg-tb08-track shadow-[inset_0_1px_2px_var(--color-tb08-inset)] active:[&_.tb-08\_\_thumb]:scale-[0.97]">
<span aria-hidden="true" class="tb-08__thumb absolute z-0 top-[0.3rem] left-[0.3rem] bottom-[0.3rem] w-[calc((100%-0.6rem)/4)] rounded-[0.7rem] bg-tb08-thumb shadow-[0_1px_2px_var(--color-tb08-shadow-1),0_6px_14px_-8px_var(--color-tb08-shadow-2)] transition-[translate,scale] duration-500 ease-tb08-spring motion-reduce:transition-none"></span>
<label class="relative z-10 flex-1 cursor-pointer grid place-items-center min-h-11 px-2 rounded-[0.7rem] text-sm font-semibold tracking-[-0.005em] text-tb08-muted transition-colors duration-200 [-webkit-tap-highlight-color:transparent] hover:text-tb08-ink-soft has-checked:text-tb08-ink has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-tb08-accent motion-reduce:transition-none"><input type="radio" name="tb-08-range" value="day" class="sr-only"><span>Day</span></label>
<label class="relative z-10 flex-1 cursor-pointer grid place-items-center min-h-11 px-2 rounded-[0.7rem] text-sm font-semibold tracking-[-0.005em] text-tb08-muted transition-colors duration-200 [-webkit-tap-highlight-color:transparent] hover:text-tb08-ink-soft has-checked:text-tb08-ink has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-tb08-accent motion-reduce:transition-none"><input type="radio" name="tb-08-range" value="week" checked class="sr-only"><span>Week</span></label>
<label class="relative z-10 flex-1 cursor-pointer grid place-items-center min-h-11 px-2 rounded-[0.7rem] text-sm font-semibold tracking-[-0.005em] text-tb08-muted transition-colors duration-200 [-webkit-tap-highlight-color:transparent] hover:text-tb08-ink-soft has-checked:text-tb08-ink has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-tb08-accent motion-reduce:transition-none"><input type="radio" name="tb-08-range" value="month" class="sr-only"><span>Month</span></label>
<label class="relative z-10 flex-1 cursor-pointer grid place-items-center min-h-11 px-2 rounded-[0.7rem] text-sm font-semibold tracking-[-0.005em] text-tb08-muted transition-colors duration-200 [-webkit-tap-highlight-color:transparent] hover:text-tb08-ink-soft has-checked:text-tb08-ink has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-tb08-accent motion-reduce:transition-none"><input type="radio" name="tb-08-range" value="year" class="sr-only"><span>Year</span></label>
</div>
</div>@import "tailwindcss";
@theme {
--color-tb08-bg: #f1f2f4;
--color-tb08-bg: oklch(0.958 0.004 265);
--color-tb08-track: #e6e7ea;
--color-tb08-track: oklch(0.925 0.005 265);
--color-tb08-thumb: #ffffff;
--color-tb08-ink: #0d0d12;
--color-tb08-ink: oklch(0.15 0.008 280);
--color-tb08-ink-soft: #3a3a45;
--color-tb08-ink-soft: oklch(0.34 0.01 280);
--color-tb08-muted: #55555f;
--color-tb08-muted: oklch(0.45 0.012 280);
--color-tb08-accent: #5b21b6;
--color-tb08-accent: oklch(0.44 0.2 296);
--color-tb08-inset: #0d0d121f;
--color-tb08-inset: oklch(0.15 0.008 280 / 0.12);
--color-tb08-shadow-1: #0d0d122e;
--color-tb08-shadow-1: oklch(0.15 0.008 280 / 0.18);
--color-tb08-shadow-2: #0d0d1266;
--color-tb08-shadow-2: oklch(0.15 0.008 280 / 0.4);
--font-tb08-sans: "Manrope", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb08-spring: cubic-bezier(0.16, 1, 0.3, 1);
}
@supports (transition-timing-function: linear(0, 1)) {
@theme {
--ease-tb08-spring: linear(0, 0.42 8%, 0.79 17%, 1.02 24%, 1.09 29%, 1.06 36%, 1 48%, 1);
}
}
.tb-08 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
/* Positional sibling state: which of four is checked. No Tailwind variant expresses this. */
.tb-08__group:has(label:nth-of-type(2) input:checked) .tb-08__thumb {
translate: 100% 0;
}
.tb-08__group:has(label:nth-of-type(3) input:checked) .tb-08__thumb {
translate: 200% 0;
}
.tb-08__group:has(label:nth-of-type(4) input:checked) .tb-08__thumb {
translate: 300% 0;
}
@supports not selector(:has(*)) {
.tb-08__thumb {
display: none;
}
.tb-08 input:checked ~ span {
color: var(--color-tb08-ink);
text-decoration: underline;
text-underline-offset: 0.4em;
}
}
@media (prefers-color-scheme: dark) {
.tb-08:not([data-theme="light"]) {
--color-tb08-bg: oklch(0.12 0.006 280);
--color-tb08-track: oklch(0.2 0.008 280);
--color-tb08-thumb: oklch(0.32 0.012 280);
--color-tb08-ink: oklch(0.97 0.003 280);
--color-tb08-ink-soft: oklch(0.85 0.006 280);
--color-tb08-muted: oklch(0.68 0.01 280);
--color-tb08-accent: oklch(0.78 0.13 296);
}
}
.tb-08[data-theme="dark"] {
--color-tb08-bg: oklch(0.12 0.006 280);
--color-tb08-track: oklch(0.2 0.008 280);
--color-tb08-thumb: oklch(0.32 0.012 280);
--color-tb08-ink: oklch(0.97 0.003 280);
--color-tb08-muted: oklch(0.68 0.01 280);
}
@media (forced-colors: active) {
.tb-08__group {
border: 1px solid ButtonText;
}
.tb-08__thumb {
background: Highlight;
}
.tb-08 label:has(input:checked) {
color: HighlightText;
}
}@import "tailwindcss";
@theme {
--color-tb08-bg: #f1f2f4;
--color-tb08-bg: oklch(0.958 0.004 265);
--color-tb08-track: #e6e7ea;
--color-tb08-track: oklch(0.925 0.005 265);
--color-tb08-thumb: #ffffff;
--color-tb08-ink: #0d0d12;
--color-tb08-ink: oklch(0.15 0.008 280);
--color-tb08-ink-soft: #3a3a45;
--color-tb08-ink-soft: oklch(0.34 0.01 280);
--color-tb08-muted: #55555f;
--color-tb08-muted: oklch(0.45 0.012 280);
--color-tb08-accent: #5b21b6;
--color-tb08-accent: oklch(0.44 0.2 296);
--color-tb08-inset: #0d0d121f;
--color-tb08-inset: oklch(0.15 0.008 280 / 0.12);
--color-tb08-shadow-1: #0d0d122e;
--color-tb08-shadow-1: oklch(0.15 0.008 280 / 0.18);
--color-tb08-shadow-2: #0d0d1266;
--color-tb08-shadow-2: oklch(0.15 0.008 280 / 0.4);
--font-tb08-sans: "Manrope", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb08-spring: cubic-bezier(0.16, 1, 0.3, 1);
}
@supports (transition-timing-function: linear(0, 1)) {
@theme {
--ease-tb08-spring: linear(0, 0.42 8%, 0.79 17%, 1.02 24%, 1.09 29%, 1.06 36%, 1 48%, 1);
}
}
.tb-08 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
/* Positional sibling state: which of four is checked. No Tailwind variant expresses this. */
.tb-08__group:has(label:nth-of-type(2) input:checked) .tb-08__thumb {
translate: 100% 0;
}
.tb-08__group:has(label:nth-of-type(3) input:checked) .tb-08__thumb {
translate: 200% 0;
}
.tb-08__group:has(label:nth-of-type(4) input:checked) .tb-08__thumb {
translate: 300% 0;
}
@supports not selector(:has(*)) {
.tb-08__thumb {
display: none;
}
.tb-08 input:checked ~ span {
color: var(--color-tb08-ink);
text-decoration: underline;
text-underline-offset: 0.4em;
}
}
@media (prefers-color-scheme: dark) {
.tb-08:not([data-theme="light"]) {
--color-tb08-bg: oklch(0.12 0.006 280);
--color-tb08-track: oklch(0.2 0.008 280);
--color-tb08-thumb: oklch(0.32 0.012 280);
--color-tb08-ink: oklch(0.97 0.003 280);
--color-tb08-ink-soft: oklch(0.85 0.006 280);
--color-tb08-muted: oklch(0.68 0.01 280);
--color-tb08-accent: oklch(0.78 0.13 296);
}
}
.tb-08[data-theme="dark"] {
--color-tb08-bg: oklch(0.12 0.006 280);
--color-tb08-track: oklch(0.2 0.008 280);
--color-tb08-thumb: oklch(0.32 0.012 280);
--color-tb08-ink: oklch(0.97 0.003 280);
--color-tb08-muted: oklch(0.68 0.01 280);
}
@media (forced-colors: active) {
.tb-08__group {
border: 1px solid ButtonText;
}
.tb-08__thumb {
background: Highlight;
}
.tb-08 label:has(input:checked) {
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-tb08-bg: #f1f2f4;
--color-tb08-bg: oklch(0.958 0.004 265);
--color-tb08-track: #e6e7ea;
--color-tb08-track: oklch(0.925 0.005 265);
--color-tb08-thumb: #ffffff;
--color-tb08-ink: #0d0d12;
--color-tb08-ink: oklch(0.15 0.008 280);
--color-tb08-ink-soft: #3a3a45;
--color-tb08-ink-soft: oklch(0.34 0.01 280);
--color-tb08-muted: #55555f;
--color-tb08-muted: oklch(0.45 0.012 280);
--color-tb08-accent: #5b21b6;
--color-tb08-accent: oklch(0.44 0.2 296);
--color-tb08-inset: #0d0d121f;
--color-tb08-inset: oklch(0.15 0.008 280 / 0.12);
--color-tb08-shadow-1: #0d0d122e;
--color-tb08-shadow-1: oklch(0.15 0.008 280 / 0.18);
--color-tb08-shadow-2: #0d0d1266;
--color-tb08-shadow-2: oklch(0.15 0.008 280 / 0.4);
--font-tb08-sans: "Manrope", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb08-spring: cubic-bezier(0.16, 1, 0.3, 1);
}
@supports (transition-timing-function: linear(0, 1)) {
@theme {
--ease-tb08-spring: linear(0, 0.42 8%, 0.79 17%, 1.02 24%, 1.09 29%, 1.06 36%, 1 48%, 1);
}
}
.tb-08 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
/* Positional sibling state: which of four is checked. No Tailwind variant expresses this. */
.tb-08__group:has(label:nth-of-type(2) input:checked) .tb-08__thumb {
translate: 100% 0;
}
.tb-08__group:has(label:nth-of-type(3) input:checked) .tb-08__thumb {
translate: 200% 0;
}
.tb-08__group:has(label:nth-of-type(4) input:checked) .tb-08__thumb {
translate: 300% 0;
}
@supports not selector(:has(*)) {
.tb-08__thumb {
display: none;
}
.tb-08 input:checked ~ span {
color: var(--color-tb08-ink);
text-decoration: underline;
text-underline-offset: 0.4em;
}
}
@media (prefers-color-scheme: dark) {
.tb-08:not([data-theme="light"]) {
--color-tb08-bg: oklch(0.12 0.006 280);
--color-tb08-track: oklch(0.2 0.008 280);
--color-tb08-thumb: oklch(0.32 0.012 280);
--color-tb08-ink: oklch(0.97 0.003 280);
--color-tb08-ink-soft: oklch(0.85 0.006 280);
--color-tb08-muted: oklch(0.68 0.01 280);
--color-tb08-accent: oklch(0.78 0.13 296);
}
}
.tb-08[data-theme="dark"] {
--color-tb08-bg: oklch(0.12 0.006 280);
--color-tb08-track: oklch(0.2 0.008 280);
--color-tb08-thumb: oklch(0.32 0.012 280);
--color-tb08-ink: oklch(0.97 0.003 280);
--color-tb08-muted: oklch(0.68 0.01 280);
}
@media (forced-colors: active) {
.tb-08__group {
border: 1px solid ButtonText;
}
.tb-08__thumb {
background: Highlight;
}
.tb-08 label:has(input:checked) {
color: HighlightText;
}
}@import "tailwindcss";
@theme {
--color-tb08-bg: #f1f2f4;
--color-tb08-bg: oklch(0.958 0.004 265);
--color-tb08-track: #e6e7ea;
--color-tb08-track: oklch(0.925 0.005 265);
--color-tb08-thumb: #ffffff;
--color-tb08-ink: #0d0d12;
--color-tb08-ink: oklch(0.15 0.008 280);
--color-tb08-ink-soft: #3a3a45;
--color-tb08-ink-soft: oklch(0.34 0.01 280);
--color-tb08-muted: #55555f;
--color-tb08-muted: oklch(0.45 0.012 280);
--color-tb08-accent: #5b21b6;
--color-tb08-accent: oklch(0.44 0.2 296);
--color-tb08-inset: #0d0d121f;
--color-tb08-inset: oklch(0.15 0.008 280 / 0.12);
--color-tb08-shadow-1: #0d0d122e;
--color-tb08-shadow-1: oklch(0.15 0.008 280 / 0.18);
--color-tb08-shadow-2: #0d0d1266;
--color-tb08-shadow-2: oklch(0.15 0.008 280 / 0.4);
--font-tb08-sans: "Manrope", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb08-spring: cubic-bezier(0.16, 1, 0.3, 1);
}
@supports (transition-timing-function: linear(0, 1)) {
@theme {
--ease-tb08-spring: linear(0, 0.42 8%, 0.79 17%, 1.02 24%, 1.09 29%, 1.06 36%, 1 48%, 1);
}
}
.tb-08 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
/* Positional sibling state: which of four is checked. No Tailwind variant expresses this. */
.tb-08__group:has(label:nth-of-type(2) input:checked) .tb-08__thumb {
translate: 100% 0;
}
.tb-08__group:has(label:nth-of-type(3) input:checked) .tb-08__thumb {
translate: 200% 0;
}
.tb-08__group:has(label:nth-of-type(4) input:checked) .tb-08__thumb {
translate: 300% 0;
}
@supports not selector(:has(*)) {
.tb-08__thumb {
display: none;
}
.tb-08 input:checked ~ span {
color: var(--color-tb08-ink);
text-decoration: underline;
text-underline-offset: 0.4em;
}
}
@media (prefers-color-scheme: dark) {
.tb-08:not([data-theme="light"]) {
--color-tb08-bg: oklch(0.12 0.006 280);
--color-tb08-track: oklch(0.2 0.008 280);
--color-tb08-thumb: oklch(0.32 0.012 280);
--color-tb08-ink: oklch(0.97 0.003 280);
--color-tb08-ink-soft: oklch(0.85 0.006 280);
--color-tb08-muted: oklch(0.68 0.01 280);
--color-tb08-accent: oklch(0.78 0.13 296);
}
}
.tb-08[data-theme="dark"] {
--color-tb08-bg: oklch(0.12 0.006 280);
--color-tb08-track: oklch(0.2 0.008 280);
--color-tb08-thumb: oklch(0.32 0.012 280);
--color-tb08-ink: oklch(0.97 0.003 280);
--color-tb08-muted: oklch(0.68 0.01 280);
}
@media (forced-colors: active) {
.tb-08__group {
border: 1px solid ButtonText;
}
.tb-08__thumb {
background: Highlight;
}
.tb-08 label:has(input:checked) {
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=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 track, the thumb's box and transition, and every per-label state —
has-checked:,has-focus-visible:, hover. CSS: the three positional:has(label:nth-of-type(n) input:checked)rules and the@supports not selector(:has(*))fallback. - This is the collection's clearest example of a port that should not go all the way. "Which of my four siblings is checked" is positional relational state; Tailwind has no variant for it, an arbitrary-selector escape hatch would be a
:has()rule written sideways, and the CSS is three readable lines. --ease-tb08-springis re-declared inside@supports (transition-timing-function: linear(0,1)), which keeps the cubic-bezier fallback and thelinear()spring behind one token name. The utilityease-tb08-springnever changes.- A11y fix from the CSS original: inactive segment labels were
oklch(0.48 0.012 280)on the track — about 4.3:1. The port moves them to0.45lightness (≈5:1) and adds a distinct--color-tb08-ink-softfor hover, so the hover state no longer computes throughcolor-mix().
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 Button Group Component (Segmented Radio-Like)
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-button-group-component-segmented/
A segmented control on real radio inputs, so arrow-key navigation, roving focus and form serialisation come from the platform. The sliding thumb tracks the checked segment, and the port makes the label's own inversion a has-checked: variant — the state is read where it is rendered, not from four rules at the bottom of a stylesheet.
## HTML
```html
<div class="tb-08 w-full min-h-svh grid place-items-center bg-tb08-bg font-tb08-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<div role="group" aria-label="Chart range" class="tb-08__group relative flex w-[min(100%,25rem)] p-[0.3rem] rounded-[1rem] bg-tb08-track shadow-[inset_0_1px_2px_var(--color-tb08-inset)] active:[&_.tb-08\_\_thumb]:scale-[0.97]">
<span aria-hidden="true" class="tb-08__thumb absolute z-0 top-[0.3rem] left-[0.3rem] bottom-[0.3rem] w-[calc((100%-0.6rem)/4)] rounded-[0.7rem] bg-tb08-thumb shadow-[0_1px_2px_var(--color-tb08-shadow-1),0_6px_14px_-8px_var(--color-tb08-shadow-2)] transition-[translate,scale] duration-500 ease-tb08-spring motion-reduce:transition-none"></span>
<label class="relative z-10 flex-1 cursor-pointer grid place-items-center min-h-11 px-2 rounded-[0.7rem] text-sm font-semibold tracking-[-0.005em] text-tb08-muted transition-colors duration-200 [-webkit-tap-highlight-color:transparent] hover:text-tb08-ink-soft has-checked:text-tb08-ink has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-tb08-accent motion-reduce:transition-none"><input type="radio" name="tb-08-range" value="day" class="sr-only"><span>Day</span></label>
<label class="relative z-10 flex-1 cursor-pointer grid place-items-center min-h-11 px-2 rounded-[0.7rem] text-sm font-semibold tracking-[-0.005em] text-tb08-muted transition-colors duration-200 [-webkit-tap-highlight-color:transparent] hover:text-tb08-ink-soft has-checked:text-tb08-ink has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-tb08-accent motion-reduce:transition-none"><input type="radio" name="tb-08-range" value="week" checked class="sr-only"><span>Week</span></label>
<label class="relative z-10 flex-1 cursor-pointer grid place-items-center min-h-11 px-2 rounded-[0.7rem] text-sm font-semibold tracking-[-0.005em] text-tb08-muted transition-colors duration-200 [-webkit-tap-highlight-color:transparent] hover:text-tb08-ink-soft has-checked:text-tb08-ink has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-tb08-accent motion-reduce:transition-none"><input type="radio" name="tb-08-range" value="month" class="sr-only"><span>Month</span></label>
<label class="relative z-10 flex-1 cursor-pointer grid place-items-center min-h-11 px-2 rounded-[0.7rem] text-sm font-semibold tracking-[-0.005em] text-tb08-muted transition-colors duration-200 [-webkit-tap-highlight-color:transparent] hover:text-tb08-ink-soft has-checked:text-tb08-ink has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-tb08-accent motion-reduce:transition-none"><input type="radio" name="tb-08-range" value="year" class="sr-only"><span>Year</span></label>
</div>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tb08-bg: #f1f2f4;
--color-tb08-bg: oklch(0.958 0.004 265);
--color-tb08-track: #e6e7ea;
--color-tb08-track: oklch(0.925 0.005 265);
--color-tb08-thumb: #ffffff;
--color-tb08-ink: #0d0d12;
--color-tb08-ink: oklch(0.15 0.008 280);
--color-tb08-ink-soft: #3a3a45;
--color-tb08-ink-soft: oklch(0.34 0.01 280);
--color-tb08-muted: #55555f;
--color-tb08-muted: oklch(0.45 0.012 280);
--color-tb08-accent: #5b21b6;
--color-tb08-accent: oklch(0.44 0.2 296);
--color-tb08-inset: #0d0d121f;
--color-tb08-inset: oklch(0.15 0.008 280 / 0.12);
--color-tb08-shadow-1: #0d0d122e;
--color-tb08-shadow-1: oklch(0.15 0.008 280 / 0.18);
--color-tb08-shadow-2: #0d0d1266;
--color-tb08-shadow-2: oklch(0.15 0.008 280 / 0.4);
--font-tb08-sans: "Manrope", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb08-spring: cubic-bezier(0.16, 1, 0.3, 1);
}
@supports (transition-timing-function: linear(0, 1)) {
@theme {
--ease-tb08-spring: linear(0, 0.42 8%, 0.79 17%, 1.02 24%, 1.09 29%, 1.06 36%, 1 48%, 1);
}
}
.tb-08 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
/* Positional sibling state: which of four is checked. No Tailwind variant expresses this. */
.tb-08__group:has(label:nth-of-type(2) input:checked) .tb-08__thumb {
translate: 100% 0;
}
.tb-08__group:has(label:nth-of-type(3) input:checked) .tb-08__thumb {
translate: 200% 0;
}
.tb-08__group:has(label:nth-of-type(4) input:checked) .tb-08__thumb {
translate: 300% 0;
}
@supports not selector(:has(*)) {
.tb-08__thumb {
display: none;
}
.tb-08 input:checked ~ span {
color: var(--color-tb08-ink);
text-decoration: underline;
text-underline-offset: 0.4em;
}
}
@media (prefers-color-scheme: dark) {
.tb-08:not([data-theme="light"]) {
--color-tb08-bg: oklch(0.12 0.006 280);
--color-tb08-track: oklch(0.2 0.008 280);
--color-tb08-thumb: oklch(0.32 0.012 280);
--color-tb08-ink: oklch(0.97 0.003 280);
--color-tb08-ink-soft: oklch(0.85 0.006 280);
--color-tb08-muted: oklch(0.68 0.01 280);
--color-tb08-accent: oklch(0.78 0.13 296);
}
}
.tb-08[data-theme="dark"] {
--color-tb08-bg: oklch(0.12 0.006 280);
--color-tb08-track: oklch(0.2 0.008 280);
--color-tb08-thumb: oklch(0.32 0.012 280);
--color-tb08-ink: oklch(0.97 0.003 280);
--color-tb08-muted: oklch(0.68 0.01 280);
}
@media (forced-colors: active) {
.tb-08__group {
border: 1px solid ButtonText;
}
.tb-08__thumb {
background: Highlight;
}
.tb-08 label:has(input:checked) {
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 Button Group Component (Segmented Radio-Like)
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-button-group-component-segmented/
A segmented control on real radio inputs, so arrow-key navigation, roving focus and form serialisation come from the platform. The sliding thumb tracks the checked segment, and the port makes the label's own inversion a has-checked: variant — the state is read where it is rendered, not from four rules at the bottom of a stylesheet.
## HTML
```html
<div class="tb-08 w-full min-h-svh grid place-items-center bg-tb08-bg font-tb08-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<div role="group" aria-label="Chart range" class="tb-08__group relative flex w-[min(100%,25rem)] p-[0.3rem] rounded-[1rem] bg-tb08-track shadow-[inset_0_1px_2px_var(--color-tb08-inset)] active:[&_.tb-08\_\_thumb]:scale-[0.97]">
<span aria-hidden="true" class="tb-08__thumb absolute z-0 top-[0.3rem] left-[0.3rem] bottom-[0.3rem] w-[calc((100%-0.6rem)/4)] rounded-[0.7rem] bg-tb08-thumb shadow-[0_1px_2px_var(--color-tb08-shadow-1),0_6px_14px_-8px_var(--color-tb08-shadow-2)] transition-[translate,scale] duration-500 ease-tb08-spring motion-reduce:transition-none"></span>
<label class="relative z-10 flex-1 cursor-pointer grid place-items-center min-h-11 px-2 rounded-[0.7rem] text-sm font-semibold tracking-[-0.005em] text-tb08-muted transition-colors duration-200 [-webkit-tap-highlight-color:transparent] hover:text-tb08-ink-soft has-checked:text-tb08-ink has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-tb08-accent motion-reduce:transition-none"><input type="radio" name="tb-08-range" value="day" class="sr-only"><span>Day</span></label>
<label class="relative z-10 flex-1 cursor-pointer grid place-items-center min-h-11 px-2 rounded-[0.7rem] text-sm font-semibold tracking-[-0.005em] text-tb08-muted transition-colors duration-200 [-webkit-tap-highlight-color:transparent] hover:text-tb08-ink-soft has-checked:text-tb08-ink has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-tb08-accent motion-reduce:transition-none"><input type="radio" name="tb-08-range" value="week" checked class="sr-only"><span>Week</span></label>
<label class="relative z-10 flex-1 cursor-pointer grid place-items-center min-h-11 px-2 rounded-[0.7rem] text-sm font-semibold tracking-[-0.005em] text-tb08-muted transition-colors duration-200 [-webkit-tap-highlight-color:transparent] hover:text-tb08-ink-soft has-checked:text-tb08-ink has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-tb08-accent motion-reduce:transition-none"><input type="radio" name="tb-08-range" value="month" class="sr-only"><span>Month</span></label>
<label class="relative z-10 flex-1 cursor-pointer grid place-items-center min-h-11 px-2 rounded-[0.7rem] text-sm font-semibold tracking-[-0.005em] text-tb08-muted transition-colors duration-200 [-webkit-tap-highlight-color:transparent] hover:text-tb08-ink-soft has-checked:text-tb08-ink has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-tb08-accent motion-reduce:transition-none"><input type="radio" name="tb-08-range" value="year" class="sr-only"><span>Year</span></label>
</div>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tb08-bg: #f1f2f4;
--color-tb08-bg: oklch(0.958 0.004 265);
--color-tb08-track: #e6e7ea;
--color-tb08-track: oklch(0.925 0.005 265);
--color-tb08-thumb: #ffffff;
--color-tb08-ink: #0d0d12;
--color-tb08-ink: oklch(0.15 0.008 280);
--color-tb08-ink-soft: #3a3a45;
--color-tb08-ink-soft: oklch(0.34 0.01 280);
--color-tb08-muted: #55555f;
--color-tb08-muted: oklch(0.45 0.012 280);
--color-tb08-accent: #5b21b6;
--color-tb08-accent: oklch(0.44 0.2 296);
--color-tb08-inset: #0d0d121f;
--color-tb08-inset: oklch(0.15 0.008 280 / 0.12);
--color-tb08-shadow-1: #0d0d122e;
--color-tb08-shadow-1: oklch(0.15 0.008 280 / 0.18);
--color-tb08-shadow-2: #0d0d1266;
--color-tb08-shadow-2: oklch(0.15 0.008 280 / 0.4);
--font-tb08-sans: "Manrope", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb08-spring: cubic-bezier(0.16, 1, 0.3, 1);
}
@supports (transition-timing-function: linear(0, 1)) {
@theme {
--ease-tb08-spring: linear(0, 0.42 8%, 0.79 17%, 1.02 24%, 1.09 29%, 1.06 36%, 1 48%, 1);
}
}
.tb-08 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
/* Positional sibling state: which of four is checked. No Tailwind variant expresses this. */
.tb-08__group:has(label:nth-of-type(2) input:checked) .tb-08__thumb {
translate: 100% 0;
}
.tb-08__group:has(label:nth-of-type(3) input:checked) .tb-08__thumb {
translate: 200% 0;
}
.tb-08__group:has(label:nth-of-type(4) input:checked) .tb-08__thumb {
translate: 300% 0;
}
@supports not selector(:has(*)) {
.tb-08__thumb {
display: none;
}
.tb-08 input:checked ~ span {
color: var(--color-tb08-ink);
text-decoration: underline;
text-underline-offset: 0.4em;
}
}
@media (prefers-color-scheme: dark) {
.tb-08:not([data-theme="light"]) {
--color-tb08-bg: oklch(0.12 0.006 280);
--color-tb08-track: oklch(0.2 0.008 280);
--color-tb08-thumb: oklch(0.32 0.012 280);
--color-tb08-ink: oklch(0.97 0.003 280);
--color-tb08-ink-soft: oklch(0.85 0.006 280);
--color-tb08-muted: oklch(0.68 0.01 280);
--color-tb08-accent: oklch(0.78 0.13 296);
}
}
.tb-08[data-theme="dark"] {
--color-tb08-bg: oklch(0.12 0.006 280);
--color-tb08-track: oklch(0.2 0.008 280);
--color-tb08-thumb: oklch(0.32 0.012 280);
--color-tb08-ink: oklch(0.97 0.003 280);
--color-tb08-muted: oklch(0.68 0.01 280);
}
@media (forced-colors: active) {
.tb-08__group {
border: 1px solid ButtonText;
}
.tb-08__thumb {
background: Highlight;
}
.tb-08 label:has(input:checked) {
color: HighlightText;
}
}
```How this works
The platform does the hard part. Each segment is a visually-hidden radio inside its label, so arrows move and select, and Tab enters and leaves the group as one stop. No JavaScript, no role="radiogroup" hand-rolling, no keydown handler.
Half the relational CSS becomes variants, half stays CSS. The per-label conditions port cleanly:
<label class="… text-tb08-muted has-checked:text-tb08-ink
has-focus-visible:outline-2 has-focus-visible:outline-tb08-accent">The thumb's position does not. It depends on which of four siblings is checked, and Tailwind has no positional sibling variant, so four :has() rules stay in the stylesheet — the one place in this demo where the CSS original is simply the better expression.
The @supports not selector(:has(*)) fallback survives untouched: no thumb, and the checked label gets an underline instead. Utilities cannot express a negative feature query, and should not try.
Make it yours
- Change the segment count: edit the thumb's
w-[calc(...)]divisor and add one:has()rule in the CSS block. - Tabs instead of a pill: give the thumb
h-0.5 top-auto bottom-1 rounded-none. - Vertical filter list:
flex-colon the group, and change the CSS translate steps to the Y axis. - Icon + label segments: put both inside the
<span>— the thumb is fraction-sized, not content-sized. - Brand it through
--color-tb08-thumband--color-tb08-ink; inactive labels stay muted for hierarchy.
Gotchas — read before shipping
- Never use
hiddenon the radio — it leaves the accessibility tree and stops being focusable.sr-onlyis the correct utility. - A segmented control is a radio group, not a row of buttons. Building it from
<button>s means writingrole="radiogroup",aria-checkedand arrow keys yourself. has-checked:matches any checked descendant. That is exactly right here (one input per label) and wrong the moment a label holds two.- Percentage
translateresolves against the thumb's own width, so the thumb must be exactly100%/nor the segments drift. - Give the group
role="group"with anaria-label— a bare radio set has no announced purpose.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
:has() (Chrome 105 / Safari 15.4 / Firefox 121) drives both the thumb and the has-* variants; the @supports not selector(:has(*)) block degrades to an underlined checked label. linear() easing needs Chrome 113 / Safari 17.2 / Firefox 112 and falls back to the declared cubic-bezier.