05 / 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 Gradient CTA Button
A conversion-grade CTA with a conic gradient ring that rotates continuously, a soft bloom underneath, and a mesh body that shifts on hover. The rotation needs @property to register the angle as an <angle>, which is the clearest case in the collection for keeping machinery in the stylesheet and exposing only the result as animate-tb05-spin.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<div class="tb-05 w-full min-h-svh grid place-items-center font-tb05-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" class="group relative isolate cursor-pointer inline-grid grid-flow-col items-center gap-2 min-h-14 px-8 border-0 rounded-2xl text-base font-semibold tracking-[-0.005em] text-white bg-[radial-gradient(120%_140%_at_15%_0%,var(--color-tb05-c1-soft),transparent_60%),radial-gradient(120%_140%_at_90%_100%,var(--color-tb05-c2-soft),transparent_60%),var(--color-tb05-body)] bg-[length:150%_150%,150%_150%,100%_100%] bg-[position:0%_0%,100%_100%,0_0] transition-[translate,background-position,scale] duration-300 ease-tb05-out before:content-[''] before:absolute before:-inset-0.5 before:-z-10 before:rounded-[inherit] before:bg-[conic-gradient(from_var(--tb-05-a),var(--color-tb05-c1),var(--color-tb05-c2),var(--color-tb05-c3),var(--color-tb05-c1))] before:animate-tb05-spin after:content-[''] after:absolute after:inset-0.5 after:-z-10 after:rounded-[inherit] after:bg-[conic-gradient(from_var(--tb-05-a),var(--color-tb05-c1),var(--color-tb05-c2),var(--color-tb05-c3),var(--color-tb05-c1))] after:blur-[20px] after:opacity-55 after:animate-tb05-spin after:transition-opacity after:duration-300 hover:-translate-y-0.5 hover:bg-[position:30%_20%,70%_80%,0_0] hover:after:opacity-90 focus-visible:after:opacity-90 active:scale-[0.98] active:translate-y-0 focus-visible:outline-2 focus-visible:outline-offset-[5px] focus-visible:outline-tb05-c2 disabled:cursor-not-allowed disabled:grayscale-[0.85] disabled:opacity-55 motion-reduce:transition-none motion-reduce:before:animate-none motion-reduce:after:animate-none">
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" class="size-[1.1rem] text-tb05-c2"><path d="M13 3 4.5 13.5H11l-1 7.5 8.5-10.5H12l1-7.5z"/></svg>
<span>Upgrade to Pro</span>
</button>
</div><div class="tb-05 w-full min-h-svh grid place-items-center font-tb05-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" class="group relative isolate cursor-pointer inline-grid grid-flow-col items-center gap-2 min-h-14 px-8 border-0 rounded-2xl text-base font-semibold tracking-[-0.005em] text-white bg-[radial-gradient(120%_140%_at_15%_0%,var(--color-tb05-c1-soft),transparent_60%),radial-gradient(120%_140%_at_90%_100%,var(--color-tb05-c2-soft),transparent_60%),var(--color-tb05-body)] bg-[length:150%_150%,150%_150%,100%_100%] bg-[position:0%_0%,100%_100%,0_0] transition-[translate,background-position,scale] duration-300 ease-tb05-out before:content-[''] before:absolute before:-inset-0.5 before:-z-10 before:rounded-[inherit] before:bg-[conic-gradient(from_var(--tb-05-a),var(--color-tb05-c1),var(--color-tb05-c2),var(--color-tb05-c3),var(--color-tb05-c1))] before:animate-tb05-spin after:content-[''] after:absolute after:inset-0.5 after:-z-10 after:rounded-[inherit] after:bg-[conic-gradient(from_var(--tb-05-a),var(--color-tb05-c1),var(--color-tb05-c2),var(--color-tb05-c3),var(--color-tb05-c1))] after:blur-[20px] after:opacity-55 after:animate-tb05-spin after:transition-opacity after:duration-300 hover:-translate-y-0.5 hover:bg-[position:30%_20%,70%_80%,0_0] hover:after:opacity-90 focus-visible:after:opacity-90 active:scale-[0.98] active:translate-y-0 focus-visible:outline-2 focus-visible:outline-offset-[5px] focus-visible:outline-tb05-c2 disabled:cursor-not-allowed disabled:grayscale-[0.85] disabled:opacity-55 motion-reduce:transition-none motion-reduce:before:animate-none motion-reduce:after:animate-none">
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" class="size-[1.1rem] text-tb05-c2"><path d="M13 3 4.5 13.5H11l-1 7.5 8.5-10.5H12l1-7.5z"/></svg>
<span>Upgrade to Pro</span>
</button>
</div>@import "tailwindcss";
@property --tb-05-a {
syntax: '<angle>';
inherits: false;
initial-value: 135deg;
}
@theme {
--color-tb05-bg: #08070d;
--color-tb05-bg: oklch(0.13 0.02 290);
--color-tb05-body: #141024;
--color-tb05-body: oklch(0.2 0.04 290);
--color-tb05-c1: #6d28d9;
--color-tb05-c1: oklch(0.52 0.24 293);
--color-tb05-c1-soft: #6d28d98c;
--color-tb05-c1-soft: oklch(0.52 0.24 293 / 0.55);
--color-tb05-c2: #2dd4bf;
--color-tb05-c2: oklch(0.8 0.14 182);
--color-tb05-c2-soft: #2dd4bf59;
--color-tb05-c2-soft: oklch(0.8 0.14 182 / 0.35);
--color-tb05-c3: #f472b6;
--color-tb05-c3: oklch(0.74 0.17 349);
--font-tb05-sans: "Sora", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb05-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb05-spin: tb-05-spin 4.5s linear infinite;
}
@keyframes tb-05-spin {
to {
--tb-05-a: 495deg;
}
}
/* Stage backdrop: a bloom behind the button plus the base colour. Two layers, one element. */
.tb-05 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(60rem 40rem at 50% 120%, color-mix(in oklab, var(--color-tb05-c1) 22%, transparent), transparent 70%), var(--color-tb05-bg);
}
.tb-05 button:disabled::before,
.tb-05 button:disabled::after {
animation-play-state: paused;
}
@media (prefers-color-scheme: light) {
.tb-05:not([data-theme="dark"]) {
--color-tb05-bg: oklch(0.96 0.01 290);
--color-tb05-body: oklch(0.24 0.06 290);
}
}
.tb-05[data-theme="light"] {
--color-tb05-bg: oklch(0.96 0.01 290);
--color-tb05-body: oklch(0.24 0.06 290);
}
@media (forced-colors: active) {
.tb-05 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
}
.tb-05 button::before,
.tb-05 button::after {
display: none;
}
}@import "tailwindcss";
@property --tb-05-a {
syntax: '<angle>';
inherits: false;
initial-value: 135deg;
}
@theme {
--color-tb05-bg: #08070d;
--color-tb05-bg: oklch(0.13 0.02 290);
--color-tb05-body: #141024;
--color-tb05-body: oklch(0.2 0.04 290);
--color-tb05-c1: #6d28d9;
--color-tb05-c1: oklch(0.52 0.24 293);
--color-tb05-c1-soft: #6d28d98c;
--color-tb05-c1-soft: oklch(0.52 0.24 293 / 0.55);
--color-tb05-c2: #2dd4bf;
--color-tb05-c2: oklch(0.8 0.14 182);
--color-tb05-c2-soft: #2dd4bf59;
--color-tb05-c2-soft: oklch(0.8 0.14 182 / 0.35);
--color-tb05-c3: #f472b6;
--color-tb05-c3: oklch(0.74 0.17 349);
--font-tb05-sans: "Sora", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb05-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb05-spin: tb-05-spin 4.5s linear infinite;
}
@keyframes tb-05-spin {
to {
--tb-05-a: 495deg;
}
}
/* Stage backdrop: a bloom behind the button plus the base colour. Two layers, one element. */
.tb-05 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(60rem 40rem at 50% 120%, color-mix(in oklab, var(--color-tb05-c1) 22%, transparent), transparent 70%), var(--color-tb05-bg);
}
.tb-05 button:disabled::before,
.tb-05 button:disabled::after {
animation-play-state: paused;
}
@media (prefers-color-scheme: light) {
.tb-05:not([data-theme="dark"]) {
--color-tb05-bg: oklch(0.96 0.01 290);
--color-tb05-body: oklch(0.24 0.06 290);
}
}
.tb-05[data-theme="light"] {
--color-tb05-bg: oklch(0.96 0.01 290);
--color-tb05-body: oklch(0.24 0.06 290);
}
@media (forced-colors: active) {
.tb-05 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
}
.tb-05 button::before,
.tb-05 button::after {
display: none;
}
}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 --tb-05-a {
syntax: '<angle>';
inherits: false;
initial-value: 135deg;
}
@theme {
--color-tb05-bg: #08070d;
--color-tb05-bg: oklch(0.13 0.02 290);
--color-tb05-body: #141024;
--color-tb05-body: oklch(0.2 0.04 290);
--color-tb05-c1: #6d28d9;
--color-tb05-c1: oklch(0.52 0.24 293);
--color-tb05-c1-soft: #6d28d98c;
--color-tb05-c1-soft: oklch(0.52 0.24 293 / 0.55);
--color-tb05-c2: #2dd4bf;
--color-tb05-c2: oklch(0.8 0.14 182);
--color-tb05-c2-soft: #2dd4bf59;
--color-tb05-c2-soft: oklch(0.8 0.14 182 / 0.35);
--color-tb05-c3: #f472b6;
--color-tb05-c3: oklch(0.74 0.17 349);
--font-tb05-sans: "Sora", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb05-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb05-spin: tb-05-spin 4.5s linear infinite;
}
@keyframes tb-05-spin {
to {
--tb-05-a: 495deg;
}
}
/* Stage backdrop: a bloom behind the button plus the base colour. Two layers, one element. */
.tb-05 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(60rem 40rem at 50% 120%, color-mix(in oklab, var(--color-tb05-c1) 22%, transparent), transparent 70%), var(--color-tb05-bg);
}
.tb-05 button:disabled::before,
.tb-05 button:disabled::after {
animation-play-state: paused;
}
@media (prefers-color-scheme: light) {
.tb-05:not([data-theme="dark"]) {
--color-tb05-bg: oklch(0.96 0.01 290);
--color-tb05-body: oklch(0.24 0.06 290);
}
}
.tb-05[data-theme="light"] {
--color-tb05-bg: oklch(0.96 0.01 290);
--color-tb05-body: oklch(0.24 0.06 290);
}
@media (forced-colors: active) {
.tb-05 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
}
.tb-05 button::before,
.tb-05 button::after {
display: none;
}
}@import "tailwindcss";
@property --tb-05-a {
syntax: '<angle>';
inherits: false;
initial-value: 135deg;
}
@theme {
--color-tb05-bg: #08070d;
--color-tb05-bg: oklch(0.13 0.02 290);
--color-tb05-body: #141024;
--color-tb05-body: oklch(0.2 0.04 290);
--color-tb05-c1: #6d28d9;
--color-tb05-c1: oklch(0.52 0.24 293);
--color-tb05-c1-soft: #6d28d98c;
--color-tb05-c1-soft: oklch(0.52 0.24 293 / 0.55);
--color-tb05-c2: #2dd4bf;
--color-tb05-c2: oklch(0.8 0.14 182);
--color-tb05-c2-soft: #2dd4bf59;
--color-tb05-c2-soft: oklch(0.8 0.14 182 / 0.35);
--color-tb05-c3: #f472b6;
--color-tb05-c3: oklch(0.74 0.17 349);
--font-tb05-sans: "Sora", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb05-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb05-spin: tb-05-spin 4.5s linear infinite;
}
@keyframes tb-05-spin {
to {
--tb-05-a: 495deg;
}
}
/* Stage backdrop: a bloom behind the button plus the base colour. Two layers, one element. */
.tb-05 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(60rem 40rem at 50% 120%, color-mix(in oklab, var(--color-tb05-c1) 22%, transparent), transparent 70%), var(--color-tb05-bg);
}
.tb-05 button:disabled::before,
.tb-05 button:disabled::after {
animation-play-state: paused;
}
@media (prefers-color-scheme: light) {
.tb-05:not([data-theme="dark"]) {
--color-tb05-bg: oklch(0.96 0.01 290);
--color-tb05-body: oklch(0.24 0.06 290);
}
}
.tb-05[data-theme="light"] {
--color-tb05-bg: oklch(0.96 0.01 290);
--color-tb05-body: oklch(0.24 0.06 290);
}
@media (forced-colors: active) {
.tb-05 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
}
.tb-05 button::before,
.tb-05 button::after {
display: none;
}
}<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=Sora:wght@400;500;600;700&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Utilities: both pseudo-element layers, the blur, the opacity states, the lift, the mesh position shift and the motion-reduce freeze. CSS: the
@propertyregistration, the keyframes, the stage backdrop (a two-layer radial on the root, which is scene-setting rather than component styling) and the disabledanimation-play-state, which has no variant form. --animate-tb05-spinin@themeis the port's best move: the animation becomes a first-class token, sobefore:animate-tb05-spinandafter:animate-tb05-spinshare one definition and the speed is tuned in one line.color-mix()inside an arbitrary gradient is legal but unreadable, so the two mixed mesh stops were promoted to--color-tb05-c1-softand--color-tb05-c2-soft. Same output, and both are now real utilities.- A11y fix from the CSS original: the bloom brightened on
:hoveronly;focus-visible:after:opacity-90gives keyboard users the same confirmation.
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 Gradient CTA Button
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-gradient-cta-button/
A conversion-grade CTA with a conic gradient ring that rotates continuously, a soft bloom underneath, and a mesh body that shifts on hover. The rotation needs @property to register the angle as an <angle>, which is the clearest case in the collection for keeping machinery in the stylesheet and exposing only the result as animate-tb05-spin.
## HTML
```html
<div class="tb-05 w-full min-h-svh grid place-items-center font-tb05-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" class="group relative isolate cursor-pointer inline-grid grid-flow-col items-center gap-2 min-h-14 px-8 border-0 rounded-2xl text-base font-semibold tracking-[-0.005em] text-white bg-[radial-gradient(120%_140%_at_15%_0%,var(--color-tb05-c1-soft),transparent_60%),radial-gradient(120%_140%_at_90%_100%,var(--color-tb05-c2-soft),transparent_60%),var(--color-tb05-body)] bg-[length:150%_150%,150%_150%,100%_100%] bg-[position:0%_0%,100%_100%,0_0] transition-[translate,background-position,scale] duration-300 ease-tb05-out before:content-[''] before:absolute before:-inset-0.5 before:-z-10 before:rounded-[inherit] before:bg-[conic-gradient(from_var(--tb-05-a),var(--color-tb05-c1),var(--color-tb05-c2),var(--color-tb05-c3),var(--color-tb05-c1))] before:animate-tb05-spin after:content-[''] after:absolute after:inset-0.5 after:-z-10 after:rounded-[inherit] after:bg-[conic-gradient(from_var(--tb-05-a),var(--color-tb05-c1),var(--color-tb05-c2),var(--color-tb05-c3),var(--color-tb05-c1))] after:blur-[20px] after:opacity-55 after:animate-tb05-spin after:transition-opacity after:duration-300 hover:-translate-y-0.5 hover:bg-[position:30%_20%,70%_80%,0_0] hover:after:opacity-90 focus-visible:after:opacity-90 active:scale-[0.98] active:translate-y-0 focus-visible:outline-2 focus-visible:outline-offset-[5px] focus-visible:outline-tb05-c2 disabled:cursor-not-allowed disabled:grayscale-[0.85] disabled:opacity-55 motion-reduce:transition-none motion-reduce:before:animate-none motion-reduce:after:animate-none">
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" class="size-[1.1rem] text-tb05-c2"><path d="M13 3 4.5 13.5H11l-1 7.5 8.5-10.5H12l1-7.5z"/></svg>
<span>Upgrade to Pro</span>
</button>
</div>
```
## CSS
```css
@import "tailwindcss";
@property --tb-05-a {
syntax: '<angle>';
inherits: false;
initial-value: 135deg;
}
@theme {
--color-tb05-bg: #08070d;
--color-tb05-bg: oklch(0.13 0.02 290);
--color-tb05-body: #141024;
--color-tb05-body: oklch(0.2 0.04 290);
--color-tb05-c1: #6d28d9;
--color-tb05-c1: oklch(0.52 0.24 293);
--color-tb05-c1-soft: #6d28d98c;
--color-tb05-c1-soft: oklch(0.52 0.24 293 / 0.55);
--color-tb05-c2: #2dd4bf;
--color-tb05-c2: oklch(0.8 0.14 182);
--color-tb05-c2-soft: #2dd4bf59;
--color-tb05-c2-soft: oklch(0.8 0.14 182 / 0.35);
--color-tb05-c3: #f472b6;
--color-tb05-c3: oklch(0.74 0.17 349);
--font-tb05-sans: "Sora", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb05-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb05-spin: tb-05-spin 4.5s linear infinite;
}
@keyframes tb-05-spin {
to {
--tb-05-a: 495deg;
}
}
/* Stage backdrop: a bloom behind the button plus the base colour. Two layers, one element. */
.tb-05 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(60rem 40rem at 50% 120%, color-mix(in oklab, var(--color-tb05-c1) 22%, transparent), transparent 70%), var(--color-tb05-bg);
}
.tb-05 button:disabled::before,
.tb-05 button:disabled::after {
animation-play-state: paused;
}
@media (prefers-color-scheme: light) {
.tb-05:not([data-theme="dark"]) {
--color-tb05-bg: oklch(0.96 0.01 290);
--color-tb05-body: oklch(0.24 0.06 290);
}
}
.tb-05[data-theme="light"] {
--color-tb05-bg: oklch(0.96 0.01 290);
--color-tb05-body: oklch(0.24 0.06 290);
}
@media (forced-colors: active) {
.tb-05 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
}
.tb-05 button::before,
.tb-05 button::after {
display: none;
}
}
```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 Gradient CTA Button
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-gradient-cta-button/
A conversion-grade CTA with a conic gradient ring that rotates continuously, a soft bloom underneath, and a mesh body that shifts on hover. The rotation needs @property to register the angle as an <angle>, which is the clearest case in the collection for keeping machinery in the stylesheet and exposing only the result as animate-tb05-spin.
## HTML
```html
<div class="tb-05 w-full min-h-svh grid place-items-center font-tb05-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" class="group relative isolate cursor-pointer inline-grid grid-flow-col items-center gap-2 min-h-14 px-8 border-0 rounded-2xl text-base font-semibold tracking-[-0.005em] text-white bg-[radial-gradient(120%_140%_at_15%_0%,var(--color-tb05-c1-soft),transparent_60%),radial-gradient(120%_140%_at_90%_100%,var(--color-tb05-c2-soft),transparent_60%),var(--color-tb05-body)] bg-[length:150%_150%,150%_150%,100%_100%] bg-[position:0%_0%,100%_100%,0_0] transition-[translate,background-position,scale] duration-300 ease-tb05-out before:content-[''] before:absolute before:-inset-0.5 before:-z-10 before:rounded-[inherit] before:bg-[conic-gradient(from_var(--tb-05-a),var(--color-tb05-c1),var(--color-tb05-c2),var(--color-tb05-c3),var(--color-tb05-c1))] before:animate-tb05-spin after:content-[''] after:absolute after:inset-0.5 after:-z-10 after:rounded-[inherit] after:bg-[conic-gradient(from_var(--tb-05-a),var(--color-tb05-c1),var(--color-tb05-c2),var(--color-tb05-c3),var(--color-tb05-c1))] after:blur-[20px] after:opacity-55 after:animate-tb05-spin after:transition-opacity after:duration-300 hover:-translate-y-0.5 hover:bg-[position:30%_20%,70%_80%,0_0] hover:after:opacity-90 focus-visible:after:opacity-90 active:scale-[0.98] active:translate-y-0 focus-visible:outline-2 focus-visible:outline-offset-[5px] focus-visible:outline-tb05-c2 disabled:cursor-not-allowed disabled:grayscale-[0.85] disabled:opacity-55 motion-reduce:transition-none motion-reduce:before:animate-none motion-reduce:after:animate-none">
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" class="size-[1.1rem] text-tb05-c2"><path d="M13 3 4.5 13.5H11l-1 7.5 8.5-10.5H12l1-7.5z"/></svg>
<span>Upgrade to Pro</span>
</button>
</div>
```
## CSS
```css
@import "tailwindcss";
@property --tb-05-a {
syntax: '<angle>';
inherits: false;
initial-value: 135deg;
}
@theme {
--color-tb05-bg: #08070d;
--color-tb05-bg: oklch(0.13 0.02 290);
--color-tb05-body: #141024;
--color-tb05-body: oklch(0.2 0.04 290);
--color-tb05-c1: #6d28d9;
--color-tb05-c1: oklch(0.52 0.24 293);
--color-tb05-c1-soft: #6d28d98c;
--color-tb05-c1-soft: oklch(0.52 0.24 293 / 0.55);
--color-tb05-c2: #2dd4bf;
--color-tb05-c2: oklch(0.8 0.14 182);
--color-tb05-c2-soft: #2dd4bf59;
--color-tb05-c2-soft: oklch(0.8 0.14 182 / 0.35);
--color-tb05-c3: #f472b6;
--color-tb05-c3: oklch(0.74 0.17 349);
--font-tb05-sans: "Sora", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb05-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb05-spin: tb-05-spin 4.5s linear infinite;
}
@keyframes tb-05-spin {
to {
--tb-05-a: 495deg;
}
}
/* Stage backdrop: a bloom behind the button plus the base colour. Two layers, one element. */
.tb-05 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(60rem 40rem at 50% 120%, color-mix(in oklab, var(--color-tb05-c1) 22%, transparent), transparent 70%), var(--color-tb05-bg);
}
.tb-05 button:disabled::before,
.tb-05 button:disabled::after {
animation-play-state: paused;
}
@media (prefers-color-scheme: light) {
.tb-05:not([data-theme="dark"]) {
--color-tb05-bg: oklch(0.96 0.01 290);
--color-tb05-body: oklch(0.24 0.06 290);
}
}
.tb-05[data-theme="light"] {
--color-tb05-bg: oklch(0.96 0.01 290);
--color-tb05-body: oklch(0.24 0.06 290);
}
@media (forced-colors: active) {
.tb-05 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
}
.tb-05 button::before,
.tb-05 button::after {
display: none;
}
}
```How this works
A custom property is a string until you register it. That is unchanged by Tailwind — @property has no utility form and belongs in the CSS block:
@property --tb-05-a{ syntax:'<angle>'; inherits:false; initial-value:135deg; }
@keyframes tb-05-spin{ to{ --tb-05-a:495deg; } }What Tailwind adds is the handle: --animate-tb05-spin: tb-05-spin 4.5s linear infinite in @theme turns the whole thing into before:animate-tb05-spin, so the duration and timing are tunable at the token layer while the type registration stays where it has to be.
Two pseudo-elements, one gradient. The ring sits at -inset-0.5 behind an opaque body, so only a 2px rim shows; the bloom is the same layer at inset-0.5 with a 20px blur. Both are pure utilities apart from the gradient itself, which is an arbitrary bg-[conic-gradient(from_var(--tb-05-a),…)].
The body shifts on hover via bg-[position:…] utilities on a two-radial mesh — the surface reads as lit rather than flat.
Make it yours
- Recolour with the three stop tokens in
@theme; within ~120° of hue reads premium, spread wide reads playful. - Speed lives in one place now: edit
--animate-tb05-spin. Under 2s it starts to read as a loading state, which is a usability bug on a CTA. - Thicken the ring:
before:-inset-[3px]. The radius follows viarounded-[inherit]. - Drop the bloom for dense dark UI with
after:opacity-0 hover:after:opacity-50. - No motion budget? Delete
before:animate-tb05-spin— the ring holds its 135° initial angle and still looks deliberate.
Gotchas — read before shipping
- Without
@propertythe keyframes snap at 50% instead of interpolating. No Tailwind utility substitutes for the registration. - Infinite motion on a primary CTA is an accessibility risk; the
motion-reduce:pair here is not optional. blur()on a large layer is fill-rate heavy — keep the bloom at the button's size and never animate the blur radius itself.- Keep the label a solid colour at ≥4.5:1 against the darkest gradient stop. Gradient text on a gradient fill fails every time.
isolateon the button is what keeps the-z-10pseudo-elements above the page background. Remove it and the ring disappears in some stacking contexts.before:animate-*requires the animation to be defined in@themeas--animate-*; an arbitrarybefore:[animation:…]works too but loses the token.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
@property needs Chrome 85 / Safari 16.4 / Firefox 128 — the same Firefox floor Tailwind v4 itself requires, so the spin is available wherever the generated sheet is. conic-gradient is universal since 2020.