16 / 25
All 25 in 25 Tailwind CSS Button Groups
- 01Tailwind Split Button with Anchor-Positioned Popover Dropdown (No JS)
- 02Tailwind Segmented Control with Motion-Path Comet Indicator
- 03Tailwind Toggle Button Group with View Transitions Morphing Highlight
- 04Tailwind Pricing Toggle — Monthly / Yearly Billing Switch (No JS)
- 05Tailwind Segmented Control with Sliding Thumb (iOS Style)
- 06Tailwind Split Button Dropdown with Native Details Menu (Save Options)
- 07Tailwind Icon Button Group Toolbar with Tooltips (Rich Text Editor)
- 08Tailwind Pill Filter Button Group with Sliding Indicator (Anchor Positioning)
- 09Tailwind Toggle Button Group for Text Alignment (Radio Inputs, Live Preview)
- 10Tailwind Web3 Connect Wallet Button Grid (Wallet Sign-In Modal)
- 11Tailwind AI Model Selector Button Group (LLM Model Picker with Badges)
- 12Tailwind Liquid Metal Segmented Control (Animated Chrome Thumb)
- 13Tailwind Tab Button Group with Subgrid Underline Indicator
- 14Tailwind Multi-Select Chip Buttons (Tag Filter Checkboxes with Count)
- 15Tailwind Animated Split Button Deploy Menu (Staggered Dropdown Reveal)
- 16Tailwind Radio Button Group with Conic Fill Animation (Export Format Picker)
- 17Tailwind Retro LED Pixel Button Group (Dot-Matrix Display Buttons)
- 18Tailwind Hold-to-Confirm Button Group with Gradient Progress (Press and Hold)
- 19Tailwind Holographic Icon Segmented Control (Grid / List View Switcher)
- 20Tailwind 3D Vertical Button Group with Hover Depth (Perspective Stack)
- 21Tailwind Floating Action Button with Radial Speed Dial Menu (No JS)
- 22Tailwind Glitch Effect Button Group (Cyberpunk RGB-Split Audio Filter)
- 23Tailwind Animated Gradient Border Pill Buttons (Rotating Conic Ring)
- 24Tailwind Button Group with Ripple Click Effect (Radar Shockwave Feedback)
- 25Tailwind Button Group with Cursor-Following Spotlight Glow (Date Range Presets)
Tailwind Radio Button Group with Conic Fill Animation (Export Format Picker)
Four format tiles — PNG, JPG, WebP, AVIF — where the chosen one fills with its own colour in a clockwise radial sweep. An @property-registered angle drives conic-gradient(from -90deg, var(--c) var(--tbg16-a), transparent 0) on a before: layer, and each tile's colour is one inline variable read by has-checked:border-(--c). Every tile now explains what the format is for.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tbg-16 w-full min-h-svh grid place-items-center bg-tbg16-bg bg-linear-160 from-tbg16-bg to-tbg16-bg2 px-5 py-14 font-tbg16-sans text-tbg16-ink" aria-label="Conic fill radio button group demo">
<div class="grid w-full max-w-[34rem] gap-3.5">
<p id="tbg16-label" class="ml-0.5 font-tbg16-mono text-xs font-semibold uppercase tracking-[0.16em] text-tbg16-mut">Export format</p>
<div role="radiogroup" aria-labelledby="tbg16-label" class="grid grid-cols-2 gap-3 sm:grid-cols-4">
<label style="--c:#ff7a8a" class="relative isolate grid min-h-20 cursor-pointer place-content-center justify-items-center gap-1 overflow-hidden rounded-2xl border-[1.5px] border-tbg16-line bg-tbg16-cell text-tbg16-mut transition-[color,border-color] duration-300 before:absolute before:inset-0 before:-z-1 before:bg-[conic-gradient(from_-90deg,var(--c)_var(--tbg16-a),transparent_0)] before:opacity-0 before:transition-opacity before:content-[''] hover:border-tbg16-hover hover:text-tbg16-ink has-checked:border-(--c) has-checked:text-tbg16-on has-checked:before:opacity-100 has-checked:before:animate-tbg16-sweep has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-(--c) motion-reduce:has-checked:before:animate-none motion-reduce:before:[--tbg16-a:360deg]"><input type="radio" name="tbg16" class="sr-only" checked><span class="text-[17px] font-extrabold tracking-[0.02em]">PNG</span><span class="font-tbg16-mono text-[11px]">Lossless</span></label>
<label style="--c:#ffbe5c" class="relative isolate grid min-h-20 cursor-pointer place-content-center justify-items-center gap-1 overflow-hidden rounded-2xl border-[1.5px] border-tbg16-line bg-tbg16-cell text-tbg16-mut transition-[color,border-color] duration-300 before:absolute before:inset-0 before:-z-1 before:bg-[conic-gradient(from_-90deg,var(--c)_var(--tbg16-a),transparent_0)] before:opacity-0 before:transition-opacity before:content-[''] hover:border-tbg16-hover hover:text-tbg16-ink has-checked:border-(--c) has-checked:text-tbg16-on has-checked:before:opacity-100 has-checked:before:animate-tbg16-sweep has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-(--c) motion-reduce:has-checked:before:animate-none motion-reduce:before:[--tbg16-a:360deg]"><input type="radio" name="tbg16" class="sr-only"><span class="text-[17px] font-extrabold tracking-[0.02em]">JPG</span><span class="font-tbg16-mono text-[11px]">Photos</span></label>
<label style="--c:#5ee0cc" class="relative isolate grid min-h-20 cursor-pointer place-content-center justify-items-center gap-1 overflow-hidden rounded-2xl border-[1.5px] border-tbg16-line bg-tbg16-cell text-tbg16-mut transition-[color,border-color] duration-300 before:absolute before:inset-0 before:-z-1 before:bg-[conic-gradient(from_-90deg,var(--c)_var(--tbg16-a),transparent_0)] before:opacity-0 before:transition-opacity before:content-[''] hover:border-tbg16-hover hover:text-tbg16-ink has-checked:border-(--c) has-checked:text-tbg16-on has-checked:before:opacity-100 has-checked:before:animate-tbg16-sweep has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-(--c) motion-reduce:has-checked:before:animate-none motion-reduce:before:[--tbg16-a:360deg]"><input type="radio" name="tbg16" class="sr-only"><span class="text-[17px] font-extrabold tracking-[0.02em]">WebP</span><span class="font-tbg16-mono text-[11px]">Web, smaller</span></label>
<label style="--c:#93abff" class="relative isolate grid min-h-20 cursor-pointer place-content-center justify-items-center gap-1 overflow-hidden rounded-2xl border-[1.5px] border-tbg16-line bg-tbg16-cell text-tbg16-mut transition-[color,border-color] duration-300 before:absolute before:inset-0 before:-z-1 before:bg-[conic-gradient(from_-90deg,var(--c)_var(--tbg16-a),transparent_0)] before:opacity-0 before:transition-opacity before:content-[''] hover:border-tbg16-hover hover:text-tbg16-ink has-checked:border-(--c) has-checked:text-tbg16-on has-checked:before:opacity-100 has-checked:before:animate-tbg16-sweep has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-(--c) motion-reduce:has-checked:before:animate-none motion-reduce:before:[--tbg16-a:360deg]"><input type="radio" name="tbg16" class="sr-only"><span class="text-[17px] font-extrabold tracking-[0.02em]">AVIF</span><span class="font-tbg16-mono text-[11px]">Smallest</span></label>
</div>
</div>
</section><section class="tbg-16 w-full min-h-svh grid place-items-center bg-tbg16-bg bg-linear-160 from-tbg16-bg to-tbg16-bg2 px-5 py-14 font-tbg16-sans text-tbg16-ink" aria-label="Conic fill radio button group demo">
<div class="grid w-full max-w-[34rem] gap-3.5">
<p id="tbg16-label" class="ml-0.5 font-tbg16-mono text-xs font-semibold uppercase tracking-[0.16em] text-tbg16-mut">Export format</p>
<div role="radiogroup" aria-labelledby="tbg16-label" class="grid grid-cols-2 gap-3 sm:grid-cols-4">
<label style="--c:#ff7a8a" class="relative isolate grid min-h-20 cursor-pointer place-content-center justify-items-center gap-1 overflow-hidden rounded-2xl border-[1.5px] border-tbg16-line bg-tbg16-cell text-tbg16-mut transition-[color,border-color] duration-300 before:absolute before:inset-0 before:-z-1 before:bg-[conic-gradient(from_-90deg,var(--c)_var(--tbg16-a),transparent_0)] before:opacity-0 before:transition-opacity before:content-[''] hover:border-tbg16-hover hover:text-tbg16-ink has-checked:border-(--c) has-checked:text-tbg16-on has-checked:before:opacity-100 has-checked:before:animate-tbg16-sweep has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-(--c) motion-reduce:has-checked:before:animate-none motion-reduce:before:[--tbg16-a:360deg]"><input type="radio" name="tbg16" class="sr-only" checked><span class="text-[17px] font-extrabold tracking-[0.02em]">PNG</span><span class="font-tbg16-mono text-[11px]">Lossless</span></label>
<label style="--c:#ffbe5c" class="relative isolate grid min-h-20 cursor-pointer place-content-center justify-items-center gap-1 overflow-hidden rounded-2xl border-[1.5px] border-tbg16-line bg-tbg16-cell text-tbg16-mut transition-[color,border-color] duration-300 before:absolute before:inset-0 before:-z-1 before:bg-[conic-gradient(from_-90deg,var(--c)_var(--tbg16-a),transparent_0)] before:opacity-0 before:transition-opacity before:content-[''] hover:border-tbg16-hover hover:text-tbg16-ink has-checked:border-(--c) has-checked:text-tbg16-on has-checked:before:opacity-100 has-checked:before:animate-tbg16-sweep has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-(--c) motion-reduce:has-checked:before:animate-none motion-reduce:before:[--tbg16-a:360deg]"><input type="radio" name="tbg16" class="sr-only"><span class="text-[17px] font-extrabold tracking-[0.02em]">JPG</span><span class="font-tbg16-mono text-[11px]">Photos</span></label>
<label style="--c:#5ee0cc" class="relative isolate grid min-h-20 cursor-pointer place-content-center justify-items-center gap-1 overflow-hidden rounded-2xl border-[1.5px] border-tbg16-line bg-tbg16-cell text-tbg16-mut transition-[color,border-color] duration-300 before:absolute before:inset-0 before:-z-1 before:bg-[conic-gradient(from_-90deg,var(--c)_var(--tbg16-a),transparent_0)] before:opacity-0 before:transition-opacity before:content-[''] hover:border-tbg16-hover hover:text-tbg16-ink has-checked:border-(--c) has-checked:text-tbg16-on has-checked:before:opacity-100 has-checked:before:animate-tbg16-sweep has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-(--c) motion-reduce:has-checked:before:animate-none motion-reduce:before:[--tbg16-a:360deg]"><input type="radio" name="tbg16" class="sr-only"><span class="text-[17px] font-extrabold tracking-[0.02em]">WebP</span><span class="font-tbg16-mono text-[11px]">Web, smaller</span></label>
<label style="--c:#93abff" class="relative isolate grid min-h-20 cursor-pointer place-content-center justify-items-center gap-1 overflow-hidden rounded-2xl border-[1.5px] border-tbg16-line bg-tbg16-cell text-tbg16-mut transition-[color,border-color] duration-300 before:absolute before:inset-0 before:-z-1 before:bg-[conic-gradient(from_-90deg,var(--c)_var(--tbg16-a),transparent_0)] before:opacity-0 before:transition-opacity before:content-[''] hover:border-tbg16-hover hover:text-tbg16-ink has-checked:border-(--c) has-checked:text-tbg16-on has-checked:before:opacity-100 has-checked:before:animate-tbg16-sweep has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-(--c) motion-reduce:has-checked:before:animate-none motion-reduce:before:[--tbg16-a:360deg]"><input type="radio" name="tbg16" class="sr-only"><span class="text-[17px] font-extrabold tracking-[0.02em]">AVIF</span><span class="font-tbg16-mono text-[11px]">Smallest</span></label>
</div>
</div>
</section>@import "tailwindcss";
/* @property is an at-rule with no utility: it makes the conic stop animatable. */
@property --tbg16-a {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@theme {
--color-tbg16-bg: #14121c;
--color-tbg16-bg2: #0c0a12;
--color-tbg16-cell: #1a1726;
--color-tbg16-line: #2a2540;
--color-tbg16-hover: #423a62;
--color-tbg16-ink: #ece7fb;
--color-tbg16-mut: #aaa2c6;
--color-tbg16-mut: oklch(0.74 0.05 290);
--color-tbg16-on: #15111f;
--font-tbg16-sans: 'Syne', system-ui, sans-serif;
--font-tbg16-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-tbg16-sweep: tbg16-sweep 0.65s cubic-bezier(0.3, 0.7, 0.3, 1) forwards;
@keyframes tbg16-sweep {
from {
--tbg16-a: 0deg;
}
to {
--tbg16-a: 360deg;
}
}
}
/* Root rule: the full-bleed contract, restated so the stage is sized before the browser build compiles. */
.tbg-16 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
/* @property is an at-rule with no utility: it makes the conic stop animatable. */
@property --tbg16-a {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@theme {
--color-tbg16-bg: #14121c;
--color-tbg16-bg2: #0c0a12;
--color-tbg16-cell: #1a1726;
--color-tbg16-line: #2a2540;
--color-tbg16-hover: #423a62;
--color-tbg16-ink: #ece7fb;
--color-tbg16-mut: #aaa2c6;
--color-tbg16-mut: oklch(0.74 0.05 290);
--color-tbg16-on: #15111f;
--font-tbg16-sans: 'Syne', system-ui, sans-serif;
--font-tbg16-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-tbg16-sweep: tbg16-sweep 0.65s cubic-bezier(0.3, 0.7, 0.3, 1) forwards;
@keyframes tbg16-sweep {
from {
--tbg16-a: 0deg;
}
to {
--tbg16-a: 360deg;
}
}
}
/* Root rule: the full-bleed contract, restated so the stage is sized before the browser build compiles. */
.tbg-16 {
width: 100%;
min-height: 100svh;
}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";
/* @property is an at-rule with no utility: it makes the conic stop animatable. */
@property --tbg16-a {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@theme {
--color-tbg16-bg: #14121c;
--color-tbg16-bg2: #0c0a12;
--color-tbg16-cell: #1a1726;
--color-tbg16-line: #2a2540;
--color-tbg16-hover: #423a62;
--color-tbg16-ink: #ece7fb;
--color-tbg16-mut: #aaa2c6;
--color-tbg16-mut: oklch(0.74 0.05 290);
--color-tbg16-on: #15111f;
--font-tbg16-sans: 'Syne', system-ui, sans-serif;
--font-tbg16-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-tbg16-sweep: tbg16-sweep 0.65s cubic-bezier(0.3, 0.7, 0.3, 1) forwards;
@keyframes tbg16-sweep {
from {
--tbg16-a: 0deg;
}
to {
--tbg16-a: 360deg;
}
}
}
/* Root rule: the full-bleed contract, restated so the stage is sized before the browser build compiles. */
.tbg-16 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
/* @property is an at-rule with no utility: it makes the conic stop animatable. */
@property --tbg16-a {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@theme {
--color-tbg16-bg: #14121c;
--color-tbg16-bg2: #0c0a12;
--color-tbg16-cell: #1a1726;
--color-tbg16-line: #2a2540;
--color-tbg16-hover: #423a62;
--color-tbg16-ink: #ece7fb;
--color-tbg16-mut: #aaa2c6;
--color-tbg16-mut: oklch(0.74 0.05 290);
--color-tbg16-on: #15111f;
--font-tbg16-sans: 'Syne', system-ui, sans-serif;
--font-tbg16-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-tbg16-sweep: tbg16-sweep 0.65s cubic-bezier(0.3, 0.7, 0.3, 1) forwards;
@keyframes tbg16-sweep {
from {
--tbg16-a: 0deg;
}
to {
--tbg16-a: 360deg;
}
}
}
/* Root rule: the full-bleed contract, restated so the stage is sized before the browser build compiles. */
.tbg-16 {
width: 100%;
min-height: 100svh;
}<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=Syne:wght@500..800&family=JetBrains+Mono:wght@400..600&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=Syne:wght@500..800&family=JetBrains+Mono:wght@400..600&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The
::beforeconic layer and its keyframes becamebefore:utilities plus an--animate-tbg16-sweeptoken;@propertystays CSS. isolation:isolate, which the original declared three different ways, is now oneisolateclass.- Contrast fix: the original put white text on yellow and mint fills (as low as 1.6:1); checked tiles now use dark ink, 9:1+ on every fill.
- UI upgrade: each format explains its use (Lossless, Photos, Web, Smallest), tiles are 80px tall, and the grid becomes 2×2 on phones.
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 Radio Button Group with Conic Fill Animation (Export Format Picker)
Source: https://codefronts.com/components/tailwind-css-button-groups/tailwind-radio-button-group-conic-fill/
Four format tiles — PNG, JPG, WebP, AVIF — where the chosen one fills with its own colour in a clockwise radial sweep. An @property-registered angle drives conic-gradient(from -90deg, var(--c) var(--tbg16-a), transparent 0) on a before: layer, and each tile's colour is one inline variable read by has-checked:border-(--c). Every tile now explains what the format is for.
## HTML
```html
<section class="tbg-16 w-full min-h-svh grid place-items-center bg-tbg16-bg bg-linear-160 from-tbg16-bg to-tbg16-bg2 px-5 py-14 font-tbg16-sans text-tbg16-ink" aria-label="Conic fill radio button group demo">
<div class="grid w-full max-w-[34rem] gap-3.5">
<p id="tbg16-label" class="ml-0.5 font-tbg16-mono text-xs font-semibold uppercase tracking-[0.16em] text-tbg16-mut">Export format</p>
<div role="radiogroup" aria-labelledby="tbg16-label" class="grid grid-cols-2 gap-3 sm:grid-cols-4">
<label style="--c:#ff7a8a" class="relative isolate grid min-h-20 cursor-pointer place-content-center justify-items-center gap-1 overflow-hidden rounded-2xl border-[1.5px] border-tbg16-line bg-tbg16-cell text-tbg16-mut transition-[color,border-color] duration-300 before:absolute before:inset-0 before:-z-1 before:bg-[conic-gradient(from_-90deg,var(--c)_var(--tbg16-a),transparent_0)] before:opacity-0 before:transition-opacity before:content-[''] hover:border-tbg16-hover hover:text-tbg16-ink has-checked:border-(--c) has-checked:text-tbg16-on has-checked:before:opacity-100 has-checked:before:animate-tbg16-sweep has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-(--c) motion-reduce:has-checked:before:animate-none motion-reduce:before:[--tbg16-a:360deg]"><input type="radio" name="tbg16" class="sr-only" checked><span class="text-[17px] font-extrabold tracking-[0.02em]">PNG</span><span class="font-tbg16-mono text-[11px]">Lossless</span></label>
<label style="--c:#ffbe5c" class="relative isolate grid min-h-20 cursor-pointer place-content-center justify-items-center gap-1 overflow-hidden rounded-2xl border-[1.5px] border-tbg16-line bg-tbg16-cell text-tbg16-mut transition-[color,border-color] duration-300 before:absolute before:inset-0 before:-z-1 before:bg-[conic-gradient(from_-90deg,var(--c)_var(--tbg16-a),transparent_0)] before:opacity-0 before:transition-opacity before:content-[''] hover:border-tbg16-hover hover:text-tbg16-ink has-checked:border-(--c) has-checked:text-tbg16-on has-checked:before:opacity-100 has-checked:before:animate-tbg16-sweep has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-(--c) motion-reduce:has-checked:before:animate-none motion-reduce:before:[--tbg16-a:360deg]"><input type="radio" name="tbg16" class="sr-only"><span class="text-[17px] font-extrabold tracking-[0.02em]">JPG</span><span class="font-tbg16-mono text-[11px]">Photos</span></label>
<label style="--c:#5ee0cc" class="relative isolate grid min-h-20 cursor-pointer place-content-center justify-items-center gap-1 overflow-hidden rounded-2xl border-[1.5px] border-tbg16-line bg-tbg16-cell text-tbg16-mut transition-[color,border-color] duration-300 before:absolute before:inset-0 before:-z-1 before:bg-[conic-gradient(from_-90deg,var(--c)_var(--tbg16-a),transparent_0)] before:opacity-0 before:transition-opacity before:content-[''] hover:border-tbg16-hover hover:text-tbg16-ink has-checked:border-(--c) has-checked:text-tbg16-on has-checked:before:opacity-100 has-checked:before:animate-tbg16-sweep has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-(--c) motion-reduce:has-checked:before:animate-none motion-reduce:before:[--tbg16-a:360deg]"><input type="radio" name="tbg16" class="sr-only"><span class="text-[17px] font-extrabold tracking-[0.02em]">WebP</span><span class="font-tbg16-mono text-[11px]">Web, smaller</span></label>
<label style="--c:#93abff" class="relative isolate grid min-h-20 cursor-pointer place-content-center justify-items-center gap-1 overflow-hidden rounded-2xl border-[1.5px] border-tbg16-line bg-tbg16-cell text-tbg16-mut transition-[color,border-color] duration-300 before:absolute before:inset-0 before:-z-1 before:bg-[conic-gradient(from_-90deg,var(--c)_var(--tbg16-a),transparent_0)] before:opacity-0 before:transition-opacity before:content-[''] hover:border-tbg16-hover hover:text-tbg16-ink has-checked:border-(--c) has-checked:text-tbg16-on has-checked:before:opacity-100 has-checked:before:animate-tbg16-sweep has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-(--c) motion-reduce:has-checked:before:animate-none motion-reduce:before:[--tbg16-a:360deg]"><input type="radio" name="tbg16" class="sr-only"><span class="text-[17px] font-extrabold tracking-[0.02em]">AVIF</span><span class="font-tbg16-mono text-[11px]">Smallest</span></label>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
/* @property is an at-rule with no utility: it makes the conic stop animatable. */
@property --tbg16-a {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@theme {
--color-tbg16-bg: #14121c;
--color-tbg16-bg2: #0c0a12;
--color-tbg16-cell: #1a1726;
--color-tbg16-line: #2a2540;
--color-tbg16-hover: #423a62;
--color-tbg16-ink: #ece7fb;
--color-tbg16-mut: #aaa2c6;
--color-tbg16-mut: oklch(0.74 0.05 290);
--color-tbg16-on: #15111f;
--font-tbg16-sans: 'Syne', system-ui, sans-serif;
--font-tbg16-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-tbg16-sweep: tbg16-sweep 0.65s cubic-bezier(0.3, 0.7, 0.3, 1) forwards;
@keyframes tbg16-sweep {
from {
--tbg16-a: 0deg;
}
to {
--tbg16-a: 360deg;
}
}
}
/* Root rule: the full-bleed contract, restated so the stage is sized before the browser build compiles. */
.tbg-16 {
width: 100%;
min-height: 100svh;
}
```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 Radio Button Group with Conic Fill Animation (Export Format Picker)
Source: https://codefronts.com/components/tailwind-css-button-groups/tailwind-radio-button-group-conic-fill/
Four format tiles — PNG, JPG, WebP, AVIF — where the chosen one fills with its own colour in a clockwise radial sweep. An @property-registered angle drives conic-gradient(from -90deg, var(--c) var(--tbg16-a), transparent 0) on a before: layer, and each tile's colour is one inline variable read by has-checked:border-(--c). Every tile now explains what the format is for.
## HTML
```html
<section class="tbg-16 w-full min-h-svh grid place-items-center bg-tbg16-bg bg-linear-160 from-tbg16-bg to-tbg16-bg2 px-5 py-14 font-tbg16-sans text-tbg16-ink" aria-label="Conic fill radio button group demo">
<div class="grid w-full max-w-[34rem] gap-3.5">
<p id="tbg16-label" class="ml-0.5 font-tbg16-mono text-xs font-semibold uppercase tracking-[0.16em] text-tbg16-mut">Export format</p>
<div role="radiogroup" aria-labelledby="tbg16-label" class="grid grid-cols-2 gap-3 sm:grid-cols-4">
<label style="--c:#ff7a8a" class="relative isolate grid min-h-20 cursor-pointer place-content-center justify-items-center gap-1 overflow-hidden rounded-2xl border-[1.5px] border-tbg16-line bg-tbg16-cell text-tbg16-mut transition-[color,border-color] duration-300 before:absolute before:inset-0 before:-z-1 before:bg-[conic-gradient(from_-90deg,var(--c)_var(--tbg16-a),transparent_0)] before:opacity-0 before:transition-opacity before:content-[''] hover:border-tbg16-hover hover:text-tbg16-ink has-checked:border-(--c) has-checked:text-tbg16-on has-checked:before:opacity-100 has-checked:before:animate-tbg16-sweep has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-(--c) motion-reduce:has-checked:before:animate-none motion-reduce:before:[--tbg16-a:360deg]"><input type="radio" name="tbg16" class="sr-only" checked><span class="text-[17px] font-extrabold tracking-[0.02em]">PNG</span><span class="font-tbg16-mono text-[11px]">Lossless</span></label>
<label style="--c:#ffbe5c" class="relative isolate grid min-h-20 cursor-pointer place-content-center justify-items-center gap-1 overflow-hidden rounded-2xl border-[1.5px] border-tbg16-line bg-tbg16-cell text-tbg16-mut transition-[color,border-color] duration-300 before:absolute before:inset-0 before:-z-1 before:bg-[conic-gradient(from_-90deg,var(--c)_var(--tbg16-a),transparent_0)] before:opacity-0 before:transition-opacity before:content-[''] hover:border-tbg16-hover hover:text-tbg16-ink has-checked:border-(--c) has-checked:text-tbg16-on has-checked:before:opacity-100 has-checked:before:animate-tbg16-sweep has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-(--c) motion-reduce:has-checked:before:animate-none motion-reduce:before:[--tbg16-a:360deg]"><input type="radio" name="tbg16" class="sr-only"><span class="text-[17px] font-extrabold tracking-[0.02em]">JPG</span><span class="font-tbg16-mono text-[11px]">Photos</span></label>
<label style="--c:#5ee0cc" class="relative isolate grid min-h-20 cursor-pointer place-content-center justify-items-center gap-1 overflow-hidden rounded-2xl border-[1.5px] border-tbg16-line bg-tbg16-cell text-tbg16-mut transition-[color,border-color] duration-300 before:absolute before:inset-0 before:-z-1 before:bg-[conic-gradient(from_-90deg,var(--c)_var(--tbg16-a),transparent_0)] before:opacity-0 before:transition-opacity before:content-[''] hover:border-tbg16-hover hover:text-tbg16-ink has-checked:border-(--c) has-checked:text-tbg16-on has-checked:before:opacity-100 has-checked:before:animate-tbg16-sweep has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-(--c) motion-reduce:has-checked:before:animate-none motion-reduce:before:[--tbg16-a:360deg]"><input type="radio" name="tbg16" class="sr-only"><span class="text-[17px] font-extrabold tracking-[0.02em]">WebP</span><span class="font-tbg16-mono text-[11px]">Web, smaller</span></label>
<label style="--c:#93abff" class="relative isolate grid min-h-20 cursor-pointer place-content-center justify-items-center gap-1 overflow-hidden rounded-2xl border-[1.5px] border-tbg16-line bg-tbg16-cell text-tbg16-mut transition-[color,border-color] duration-300 before:absolute before:inset-0 before:-z-1 before:bg-[conic-gradient(from_-90deg,var(--c)_var(--tbg16-a),transparent_0)] before:opacity-0 before:transition-opacity before:content-[''] hover:border-tbg16-hover hover:text-tbg16-ink has-checked:border-(--c) has-checked:text-tbg16-on has-checked:before:opacity-100 has-checked:before:animate-tbg16-sweep has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-(--c) motion-reduce:has-checked:before:animate-none motion-reduce:before:[--tbg16-a:360deg]"><input type="radio" name="tbg16" class="sr-only"><span class="text-[17px] font-extrabold tracking-[0.02em]">AVIF</span><span class="font-tbg16-mono text-[11px]">Smallest</span></label>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
/* @property is an at-rule with no utility: it makes the conic stop animatable. */
@property --tbg16-a {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@theme {
--color-tbg16-bg: #14121c;
--color-tbg16-bg2: #0c0a12;
--color-tbg16-cell: #1a1726;
--color-tbg16-line: #2a2540;
--color-tbg16-hover: #423a62;
--color-tbg16-ink: #ece7fb;
--color-tbg16-mut: #aaa2c6;
--color-tbg16-mut: oklch(0.74 0.05 290);
--color-tbg16-on: #15111f;
--font-tbg16-sans: 'Syne', system-ui, sans-serif;
--font-tbg16-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-tbg16-sweep: tbg16-sweep 0.65s cubic-bezier(0.3, 0.7, 0.3, 1) forwards;
@keyframes tbg16-sweep {
from {
--tbg16-a: 0deg;
}
to {
--tbg16-a: 360deg;
}
}
}
/* Root rule: the full-bleed contract, restated so the stage is sized before the browser build compiles. */
.tbg-16 {
width: 100%;
min-height: 100svh;
}
```How this works
Animating a gradient stop. Gradients can't be transitioned, but a registered angle inside one can:
@property --tbg16-a { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
<label style="--c:#ff5d73" class="isolate before:-z-1
before:bg-[conic-gradient(from_-90deg,var(--c)_var(--tbg16-a),transparent_0)]
has-checked:before:animate-tbg16-sweep">The animation token ends forwards, so the tile stays fully filled until another one is picked, which replays the sweep there.
Make it yours
- Counter-clockwise:
from_-90deg→from_90degand animate 360 → 0. - Two-tone sweep: add a second stop, e.g.
var(--c)_var(--tbg16-a),color-mix(…)_0. - Change formats: copy a tile, set its
--c, name and description.
Gotchas — read before shipping
@propertymust be registered outside@theme— it is an at-rule, not a token.- Without
isolateon the tile,before:-z-1would slide under the stage background. - Light fills need dark text: checked tiles switch to
text-tbg16-on.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
@property is Baseline 2024. Without it the tile fills instantly instead of sweeping.