28 / 28
All 28 in 28 Tailwind CSS Close Buttons
- 01Tailwind Modal Close Button (Top-Right Corner X)
- 02Accessible Tailwind Close Button with Focus Ring (WCAG)
- 03iOS-Style Tailwind Tab Close Button
- 04Tailwind Floating Chat Widget Close Button
- 05Tailwind Cookie Consent Banner Dismiss Button
- 06Tailwind Mobile Ad Banner Close Button (48px Tap Target)
- 07Neumorphic Tailwind Close Button (Soft UI)
- 08Tailwind Hamburger to X Menu Button Animation
- 09Tailwind Video Player Overlay Close Button
- 10Tailwind Drawer Close Button with Native Dialog
- 11Origami Fold Tailwind Close Button
- 12Lock Tumbler Tailwind Close Button (@property)
- 13Rotate on Hover Tailwind Close Button
- 14Magnetic Cursor Tailwind Close Button
- 15Liquid Goo Tailwind Close Button (SVG Filter)
- 16Glitch Effect Tailwind Close Button
- 17Trash Compactor Tailwind Close Button
- 18Material Ripple Tailwind Close Button
- 19Particle Burst Tailwind Close Button
- 20Spring Snap Tailwind Close Button
- 21Neon Glow Tailwind Close Button
- 22Corner Ribbon Tailwind Close Button
- 233D Fold Away Tailwind Close Button
- 24Hold to Confirm Tailwind Close Button
- 25Tailwind Toast Notification Dismiss Button
- 26Tailwind Lightbox Image Close Button
- 27Brutalist Tailwind Close Button
- 28Galaxy Vortex Tailwind Close Button
Galaxy Vortex Tailwind Close Button
A spiral galaxy in a button: two conic-gradient arm layers counter-rotate at co-prime speeds (11s and 7s) via two typed @property angles, a ten-star field is a single box-shadow, and hover whips both arms up to 3.5s/2.2s with group-hover:[animation-duration:3.5s] as the × slowly spins and shrinks into the vortex.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tcb-28 w-full min-h-svh grid place-items-center bg-[radial-gradient(100%_120%_at_50%_0%,var(--color-tcb28-bg),var(--color-tcb28-bg2))] px-6 py-12 font-tcb28-sans" aria-label="Galaxy vortex close button demo">
<div class="grid w-full justify-items-center gap-7">
<button type="button" aria-label="Close" class="group relative grid size-[110px] cursor-pointer place-items-center overflow-hidden rounded-full bg-tcb28-core shadow-[0_0_0_1px_var(--color-tcb28-rim),0_24px_60px_-24px_var(--color-tcb28-glow)] focus-visible:outline-[2.5px] focus-visible:outline-offset-4 focus-visible:outline-tcb28-arm-b active:scale-94">
<span aria-hidden="true" class="absolute inset-0 rounded-full bg-[conic-gradient(from_var(--tcb28-a),transparent_0_40deg,var(--color-tcb28-arm-a)_60deg_80deg,transparent_100deg_160deg,var(--color-tcb28-arm-a)_185deg_205deg,transparent_225deg_290deg,var(--color-tcb28-arm-a)_310deg_330deg,transparent_350deg)] [mask:radial-gradient(circle,#000_0_70%,transparent_71%)] animate-tcb28-arm-a group-hover:[animation-duration:3.5s] group-focus-visible:[animation-duration:3.5s] motion-reduce:animate-none"></span>
<span aria-hidden="true" class="absolute inset-0 rounded-full bg-[conic-gradient(from_var(--tcb28-b),transparent_0_70deg,var(--color-tcb28-arm-b2)_90deg_105deg,transparent_125deg_200deg,var(--color-tcb28-arm-b2)_225deg_245deg,transparent_265deg)] [mask:radial-gradient(circle,#000_0_55%,transparent_56%)] animate-tcb28-arm-b group-hover:[animation-duration:2.2s] group-focus-visible:[animation-duration:2.2s] motion-reduce:animate-none"></span>
<span aria-hidden="true" class="absolute top-[18%] left-[22%] size-[1.5px] rounded-full bg-white shadow-[12px_30px_0_0_rgb(255_255_255/0.8),34px_8px_0_-0.4px_rgb(255_255_255/0.6),55px_42px_0_0_rgb(255_255_255/0.9),70px_15px_0_-0.5px_rgb(255_255_255/0.5),20px_58px_0_-0.4px_rgb(255_255_255/0.7),46px_66px_0_0_rgb(255_255_255/0.6),64px_55px_0_-0.6px_rgb(255_255_255/0.45),6px_44px_0_-0.5px_rgb(255_255_255/0.55),40px_24px_0_-0.6px_rgb(255_255_255/0.5),58px_4px_0_-0.4px_rgb(255_255_255/0.65)]"></span>
<span aria-hidden="true" class="relative size-5.5 drop-shadow-[0_0_6px_var(--color-tcb28-arm-b)] transition-[rotate,scale] duration-[2.2s] ease-[cubic-bezier(.3,0,.2,1)] before:absolute before:top-1/2 before:left-1/2 before:h-[2.5px] before:w-5.5 before:-translate-1/2 before:rotate-45 before:rounded-full before:bg-white after:absolute after:top-1/2 after:left-1/2 after:h-[2.5px] after:w-5.5 after:-translate-1/2 after:-rotate-45 after:rounded-full after:bg-white group-hover:rotate-180 group-hover:scale-75 group-focus-visible:rotate-180 group-focus-visible:scale-75 motion-reduce:transition-none"></span>
</button>
<p aria-hidden="true" class="rounded-full px-3.5 py-2 font-tcb28-mono text-[11.5px] text-tcb28-mut ring-1 ring-white/12">2× @property angles · 11s / 7s co-prime · box-shadow starfield</p>
</div>
</section><section class="tcb-28 w-full min-h-svh grid place-items-center bg-[radial-gradient(100%_120%_at_50%_0%,var(--color-tcb28-bg),var(--color-tcb28-bg2))] px-6 py-12 font-tcb28-sans" aria-label="Galaxy vortex close button demo">
<div class="grid w-full justify-items-center gap-7">
<button type="button" aria-label="Close" class="group relative grid size-[110px] cursor-pointer place-items-center overflow-hidden rounded-full bg-tcb28-core shadow-[0_0_0_1px_var(--color-tcb28-rim),0_24px_60px_-24px_var(--color-tcb28-glow)] focus-visible:outline-[2.5px] focus-visible:outline-offset-4 focus-visible:outline-tcb28-arm-b active:scale-94">
<span aria-hidden="true" class="absolute inset-0 rounded-full bg-[conic-gradient(from_var(--tcb28-a),transparent_0_40deg,var(--color-tcb28-arm-a)_60deg_80deg,transparent_100deg_160deg,var(--color-tcb28-arm-a)_185deg_205deg,transparent_225deg_290deg,var(--color-tcb28-arm-a)_310deg_330deg,transparent_350deg)] [mask:radial-gradient(circle,#000_0_70%,transparent_71%)] animate-tcb28-arm-a group-hover:[animation-duration:3.5s] group-focus-visible:[animation-duration:3.5s] motion-reduce:animate-none"></span>
<span aria-hidden="true" class="absolute inset-0 rounded-full bg-[conic-gradient(from_var(--tcb28-b),transparent_0_70deg,var(--color-tcb28-arm-b2)_90deg_105deg,transparent_125deg_200deg,var(--color-tcb28-arm-b2)_225deg_245deg,transparent_265deg)] [mask:radial-gradient(circle,#000_0_55%,transparent_56%)] animate-tcb28-arm-b group-hover:[animation-duration:2.2s] group-focus-visible:[animation-duration:2.2s] motion-reduce:animate-none"></span>
<span aria-hidden="true" class="absolute top-[18%] left-[22%] size-[1.5px] rounded-full bg-white shadow-[12px_30px_0_0_rgb(255_255_255/0.8),34px_8px_0_-0.4px_rgb(255_255_255/0.6),55px_42px_0_0_rgb(255_255_255/0.9),70px_15px_0_-0.5px_rgb(255_255_255/0.5),20px_58px_0_-0.4px_rgb(255_255_255/0.7),46px_66px_0_0_rgb(255_255_255/0.6),64px_55px_0_-0.6px_rgb(255_255_255/0.45),6px_44px_0_-0.5px_rgb(255_255_255/0.55),40px_24px_0_-0.6px_rgb(255_255_255/0.5),58px_4px_0_-0.4px_rgb(255_255_255/0.65)]"></span>
<span aria-hidden="true" class="relative size-5.5 drop-shadow-[0_0_6px_var(--color-tcb28-arm-b)] transition-[rotate,scale] duration-[2.2s] ease-[cubic-bezier(.3,0,.2,1)] before:absolute before:top-1/2 before:left-1/2 before:h-[2.5px] before:w-5.5 before:-translate-1/2 before:rotate-45 before:rounded-full before:bg-white after:absolute after:top-1/2 after:left-1/2 after:h-[2.5px] after:w-5.5 after:-translate-1/2 after:-rotate-45 after:rounded-full after:bg-white group-hover:rotate-180 group-hover:scale-75 group-focus-visible:rotate-180 group-focus-visible:scale-75 motion-reduce:transition-none"></span>
</button>
<p aria-hidden="true" class="rounded-full px-3.5 py-2 font-tcb28-mono text-[11.5px] text-tcb28-mut ring-1 ring-white/12">2× @property angles · 11s / 7s co-prime · box-shadow starfield</p>
</div>
</section>@import "tailwindcss";
/* The one rule utilities cannot express: typed angle properties, so keyframes can rotate a conic gradient. */
@property --tcb28-a {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@property --tcb28-b {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@theme {
--color-tcb28-bg: #1a1328;
--color-tcb28-bg: oklch(0.17 0.03 290);
--color-tcb28-bg2: #0b0911;
--color-tcb28-bg2: oklch(0.1 0.015 280);
--color-tcb28-core: #120e1c;
--color-tcb28-core: oklch(0.14 0.02 290);
--color-tcb28-rim: #8a4fd840;
--color-tcb28-rim: oklch(0.6 0.2 300 / 0.25);
--color-tcb28-glow: #7a3fc799;
--color-tcb28-glow: oklch(0.5 0.2 300 / 0.6);
--color-tcb28-arm-a: #8a4fd859;
--color-tcb28-arm-a: oklch(0.6 0.2 300 / 0.35);
--color-tcb28-arm-b: #5fb0e6;
--color-tcb28-arm-b: oklch(0.72 0.12 225);
--color-tcb28-arm-b2: #5fb0e64d;
--color-tcb28-arm-b2: oklch(0.72 0.12 225 / 0.3);
--color-tcb28-mut: #b5a9cc;
--color-tcb28-mut: oklch(0.76 0.05 295);
--font-tcb28-sans: 'Geist', system-ui, sans-serif;
--font-tcb28-mono: 'Geist Mono', ui-monospace, monospace;
--animate-tcb28-arm-a: tcb28-arm-a 11s linear infinite;
--animate-tcb28-arm-b: tcb28-arm-b 7s linear infinite reverse;
@keyframes tcb28-arm-a {
to {
--tcb28-a: 360deg;
}
}
@keyframes tcb28-arm-b {
to {
--tcb28-b: 360deg;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tcb-28 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
/* The one rule utilities cannot express: typed angle properties, so keyframes can rotate a conic gradient. */
@property --tcb28-a {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@property --tcb28-b {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@theme {
--color-tcb28-bg: #1a1328;
--color-tcb28-bg: oklch(0.17 0.03 290);
--color-tcb28-bg2: #0b0911;
--color-tcb28-bg2: oklch(0.1 0.015 280);
--color-tcb28-core: #120e1c;
--color-tcb28-core: oklch(0.14 0.02 290);
--color-tcb28-rim: #8a4fd840;
--color-tcb28-rim: oklch(0.6 0.2 300 / 0.25);
--color-tcb28-glow: #7a3fc799;
--color-tcb28-glow: oklch(0.5 0.2 300 / 0.6);
--color-tcb28-arm-a: #8a4fd859;
--color-tcb28-arm-a: oklch(0.6 0.2 300 / 0.35);
--color-tcb28-arm-b: #5fb0e6;
--color-tcb28-arm-b: oklch(0.72 0.12 225);
--color-tcb28-arm-b2: #5fb0e64d;
--color-tcb28-arm-b2: oklch(0.72 0.12 225 / 0.3);
--color-tcb28-mut: #b5a9cc;
--color-tcb28-mut: oklch(0.76 0.05 295);
--font-tcb28-sans: 'Geist', system-ui, sans-serif;
--font-tcb28-mono: 'Geist Mono', ui-monospace, monospace;
--animate-tcb28-arm-a: tcb28-arm-a 11s linear infinite;
--animate-tcb28-arm-b: tcb28-arm-b 7s linear infinite reverse;
@keyframes tcb28-arm-a {
to {
--tcb28-a: 360deg;
}
}
@keyframes tcb28-arm-b {
to {
--tcb28-b: 360deg;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tcb-28 {
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";
/* The one rule utilities cannot express: typed angle properties, so keyframes can rotate a conic gradient. */
@property --tcb28-a {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@property --tcb28-b {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@theme {
--color-tcb28-bg: #1a1328;
--color-tcb28-bg: oklch(0.17 0.03 290);
--color-tcb28-bg2: #0b0911;
--color-tcb28-bg2: oklch(0.1 0.015 280);
--color-tcb28-core: #120e1c;
--color-tcb28-core: oklch(0.14 0.02 290);
--color-tcb28-rim: #8a4fd840;
--color-tcb28-rim: oklch(0.6 0.2 300 / 0.25);
--color-tcb28-glow: #7a3fc799;
--color-tcb28-glow: oklch(0.5 0.2 300 / 0.6);
--color-tcb28-arm-a: #8a4fd859;
--color-tcb28-arm-a: oklch(0.6 0.2 300 / 0.35);
--color-tcb28-arm-b: #5fb0e6;
--color-tcb28-arm-b: oklch(0.72 0.12 225);
--color-tcb28-arm-b2: #5fb0e64d;
--color-tcb28-arm-b2: oklch(0.72 0.12 225 / 0.3);
--color-tcb28-mut: #b5a9cc;
--color-tcb28-mut: oklch(0.76 0.05 295);
--font-tcb28-sans: 'Geist', system-ui, sans-serif;
--font-tcb28-mono: 'Geist Mono', ui-monospace, monospace;
--animate-tcb28-arm-a: tcb28-arm-a 11s linear infinite;
--animate-tcb28-arm-b: tcb28-arm-b 7s linear infinite reverse;
@keyframes tcb28-arm-a {
to {
--tcb28-a: 360deg;
}
}
@keyframes tcb28-arm-b {
to {
--tcb28-b: 360deg;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tcb-28 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
/* The one rule utilities cannot express: typed angle properties, so keyframes can rotate a conic gradient. */
@property --tcb28-a {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@property --tcb28-b {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@theme {
--color-tcb28-bg: #1a1328;
--color-tcb28-bg: oklch(0.17 0.03 290);
--color-tcb28-bg2: #0b0911;
--color-tcb28-bg2: oklch(0.1 0.015 280);
--color-tcb28-core: #120e1c;
--color-tcb28-core: oklch(0.14 0.02 290);
--color-tcb28-rim: #8a4fd840;
--color-tcb28-rim: oklch(0.6 0.2 300 / 0.25);
--color-tcb28-glow: #7a3fc799;
--color-tcb28-glow: oklch(0.5 0.2 300 / 0.6);
--color-tcb28-arm-a: #8a4fd859;
--color-tcb28-arm-a: oklch(0.6 0.2 300 / 0.35);
--color-tcb28-arm-b: #5fb0e6;
--color-tcb28-arm-b: oklch(0.72 0.12 225);
--color-tcb28-arm-b2: #5fb0e64d;
--color-tcb28-arm-b2: oklch(0.72 0.12 225 / 0.3);
--color-tcb28-mut: #b5a9cc;
--color-tcb28-mut: oklch(0.76 0.05 295);
--font-tcb28-sans: 'Geist', system-ui, sans-serif;
--font-tcb28-mono: 'Geist Mono', ui-monospace, monospace;
--animate-tcb28-arm-a: tcb28-arm-a 11s linear infinite;
--animate-tcb28-arm-b: tcb28-arm-b 7s linear infinite reverse;
@keyframes tcb28-arm-a {
to {
--tcb28-a: 360deg;
}
}
@keyframes tcb28-arm-b {
to {
--tcb28-b: 360deg;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tcb-28 {
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=Geist:wght@400..800&family=Geist+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=Geist:wght@400..800&family=Geist+Mono:wght@400..600&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Both spiral animations became
@themeentries; the hover speed-up is an arbitrary property,group-hover:[animation-duration:3.5s], so the arms accelerate without resetting their angle. - Every inline
oklch(… / .3)arm colour became a named translucent token (--color-tcb28-arm-a,--color-tcb28-arm-b2), so re-theming the galaxy is a two-line edit. - UI upgrade from the CSS original: keyboard focus accelerates the vortex and spins the × just like hover, and the stage caption rose to 7:1.
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: Galaxy Vortex Tailwind Close Button
Source: https://codefronts.com/components/tailwind-css-close-buttons/galaxy-vortex/
A spiral galaxy in a button: two conic-gradient arm layers counter-rotate at co-prime speeds (11s and 7s) via two typed @property angles, a ten-star field is a single box-shadow, and hover whips both arms up to 3.5s/2.2s with group-hover:[animation-duration:3.5s] as the × slowly spins and shrinks into the vortex.
## HTML
```html
<section class="tcb-28 w-full min-h-svh grid place-items-center bg-[radial-gradient(100%_120%_at_50%_0%,var(--color-tcb28-bg),var(--color-tcb28-bg2))] px-6 py-12 font-tcb28-sans" aria-label="Galaxy vortex close button demo">
<div class="grid w-full justify-items-center gap-7">
<button type="button" aria-label="Close" class="group relative grid size-[110px] cursor-pointer place-items-center overflow-hidden rounded-full bg-tcb28-core shadow-[0_0_0_1px_var(--color-tcb28-rim),0_24px_60px_-24px_var(--color-tcb28-glow)] focus-visible:outline-[2.5px] focus-visible:outline-offset-4 focus-visible:outline-tcb28-arm-b active:scale-94">
<span aria-hidden="true" class="absolute inset-0 rounded-full bg-[conic-gradient(from_var(--tcb28-a),transparent_0_40deg,var(--color-tcb28-arm-a)_60deg_80deg,transparent_100deg_160deg,var(--color-tcb28-arm-a)_185deg_205deg,transparent_225deg_290deg,var(--color-tcb28-arm-a)_310deg_330deg,transparent_350deg)] [mask:radial-gradient(circle,#000_0_70%,transparent_71%)] animate-tcb28-arm-a group-hover:[animation-duration:3.5s] group-focus-visible:[animation-duration:3.5s] motion-reduce:animate-none"></span>
<span aria-hidden="true" class="absolute inset-0 rounded-full bg-[conic-gradient(from_var(--tcb28-b),transparent_0_70deg,var(--color-tcb28-arm-b2)_90deg_105deg,transparent_125deg_200deg,var(--color-tcb28-arm-b2)_225deg_245deg,transparent_265deg)] [mask:radial-gradient(circle,#000_0_55%,transparent_56%)] animate-tcb28-arm-b group-hover:[animation-duration:2.2s] group-focus-visible:[animation-duration:2.2s] motion-reduce:animate-none"></span>
<span aria-hidden="true" class="absolute top-[18%] left-[22%] size-[1.5px] rounded-full bg-white shadow-[12px_30px_0_0_rgb(255_255_255/0.8),34px_8px_0_-0.4px_rgb(255_255_255/0.6),55px_42px_0_0_rgb(255_255_255/0.9),70px_15px_0_-0.5px_rgb(255_255_255/0.5),20px_58px_0_-0.4px_rgb(255_255_255/0.7),46px_66px_0_0_rgb(255_255_255/0.6),64px_55px_0_-0.6px_rgb(255_255_255/0.45),6px_44px_0_-0.5px_rgb(255_255_255/0.55),40px_24px_0_-0.6px_rgb(255_255_255/0.5),58px_4px_0_-0.4px_rgb(255_255_255/0.65)]"></span>
<span aria-hidden="true" class="relative size-5.5 drop-shadow-[0_0_6px_var(--color-tcb28-arm-b)] transition-[rotate,scale] duration-[2.2s] ease-[cubic-bezier(.3,0,.2,1)] before:absolute before:top-1/2 before:left-1/2 before:h-[2.5px] before:w-5.5 before:-translate-1/2 before:rotate-45 before:rounded-full before:bg-white after:absolute after:top-1/2 after:left-1/2 after:h-[2.5px] after:w-5.5 after:-translate-1/2 after:-rotate-45 after:rounded-full after:bg-white group-hover:rotate-180 group-hover:scale-75 group-focus-visible:rotate-180 group-focus-visible:scale-75 motion-reduce:transition-none"></span>
</button>
<p aria-hidden="true" class="rounded-full px-3.5 py-2 font-tcb28-mono text-[11.5px] text-tcb28-mut ring-1 ring-white/12">2× @property angles · 11s / 7s co-prime · box-shadow starfield</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
/* The one rule utilities cannot express: typed angle properties, so keyframes can rotate a conic gradient. */
@property --tcb28-a {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@property --tcb28-b {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@theme {
--color-tcb28-bg: #1a1328;
--color-tcb28-bg: oklch(0.17 0.03 290);
--color-tcb28-bg2: #0b0911;
--color-tcb28-bg2: oklch(0.1 0.015 280);
--color-tcb28-core: #120e1c;
--color-tcb28-core: oklch(0.14 0.02 290);
--color-tcb28-rim: #8a4fd840;
--color-tcb28-rim: oklch(0.6 0.2 300 / 0.25);
--color-tcb28-glow: #7a3fc799;
--color-tcb28-glow: oklch(0.5 0.2 300 / 0.6);
--color-tcb28-arm-a: #8a4fd859;
--color-tcb28-arm-a: oklch(0.6 0.2 300 / 0.35);
--color-tcb28-arm-b: #5fb0e6;
--color-tcb28-arm-b: oklch(0.72 0.12 225);
--color-tcb28-arm-b2: #5fb0e64d;
--color-tcb28-arm-b2: oklch(0.72 0.12 225 / 0.3);
--color-tcb28-mut: #b5a9cc;
--color-tcb28-mut: oklch(0.76 0.05 295);
--font-tcb28-sans: 'Geist', system-ui, sans-serif;
--font-tcb28-mono: 'Geist Mono', ui-monospace, monospace;
--animate-tcb28-arm-a: tcb28-arm-a 11s linear infinite;
--animate-tcb28-arm-b: tcb28-arm-b 7s linear infinite reverse;
@keyframes tcb28-arm-a {
to {
--tcb28-a: 360deg;
}
}
@keyframes tcb28-arm-b {
to {
--tcb28-b: 360deg;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tcb-28 {
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: Galaxy Vortex Tailwind Close Button
Source: https://codefronts.com/components/tailwind-css-close-buttons/galaxy-vortex/
A spiral galaxy in a button: two conic-gradient arm layers counter-rotate at co-prime speeds (11s and 7s) via two typed @property angles, a ten-star field is a single box-shadow, and hover whips both arms up to 3.5s/2.2s with group-hover:[animation-duration:3.5s] as the × slowly spins and shrinks into the vortex.
## HTML
```html
<section class="tcb-28 w-full min-h-svh grid place-items-center bg-[radial-gradient(100%_120%_at_50%_0%,var(--color-tcb28-bg),var(--color-tcb28-bg2))] px-6 py-12 font-tcb28-sans" aria-label="Galaxy vortex close button demo">
<div class="grid w-full justify-items-center gap-7">
<button type="button" aria-label="Close" class="group relative grid size-[110px] cursor-pointer place-items-center overflow-hidden rounded-full bg-tcb28-core shadow-[0_0_0_1px_var(--color-tcb28-rim),0_24px_60px_-24px_var(--color-tcb28-glow)] focus-visible:outline-[2.5px] focus-visible:outline-offset-4 focus-visible:outline-tcb28-arm-b active:scale-94">
<span aria-hidden="true" class="absolute inset-0 rounded-full bg-[conic-gradient(from_var(--tcb28-a),transparent_0_40deg,var(--color-tcb28-arm-a)_60deg_80deg,transparent_100deg_160deg,var(--color-tcb28-arm-a)_185deg_205deg,transparent_225deg_290deg,var(--color-tcb28-arm-a)_310deg_330deg,transparent_350deg)] [mask:radial-gradient(circle,#000_0_70%,transparent_71%)] animate-tcb28-arm-a group-hover:[animation-duration:3.5s] group-focus-visible:[animation-duration:3.5s] motion-reduce:animate-none"></span>
<span aria-hidden="true" class="absolute inset-0 rounded-full bg-[conic-gradient(from_var(--tcb28-b),transparent_0_70deg,var(--color-tcb28-arm-b2)_90deg_105deg,transparent_125deg_200deg,var(--color-tcb28-arm-b2)_225deg_245deg,transparent_265deg)] [mask:radial-gradient(circle,#000_0_55%,transparent_56%)] animate-tcb28-arm-b group-hover:[animation-duration:2.2s] group-focus-visible:[animation-duration:2.2s] motion-reduce:animate-none"></span>
<span aria-hidden="true" class="absolute top-[18%] left-[22%] size-[1.5px] rounded-full bg-white shadow-[12px_30px_0_0_rgb(255_255_255/0.8),34px_8px_0_-0.4px_rgb(255_255_255/0.6),55px_42px_0_0_rgb(255_255_255/0.9),70px_15px_0_-0.5px_rgb(255_255_255/0.5),20px_58px_0_-0.4px_rgb(255_255_255/0.7),46px_66px_0_0_rgb(255_255_255/0.6),64px_55px_0_-0.6px_rgb(255_255_255/0.45),6px_44px_0_-0.5px_rgb(255_255_255/0.55),40px_24px_0_-0.6px_rgb(255_255_255/0.5),58px_4px_0_-0.4px_rgb(255_255_255/0.65)]"></span>
<span aria-hidden="true" class="relative size-5.5 drop-shadow-[0_0_6px_var(--color-tcb28-arm-b)] transition-[rotate,scale] duration-[2.2s] ease-[cubic-bezier(.3,0,.2,1)] before:absolute before:top-1/2 before:left-1/2 before:h-[2.5px] before:w-5.5 before:-translate-1/2 before:rotate-45 before:rounded-full before:bg-white after:absolute after:top-1/2 after:left-1/2 after:h-[2.5px] after:w-5.5 after:-translate-1/2 after:-rotate-45 after:rounded-full after:bg-white group-hover:rotate-180 group-hover:scale-75 group-focus-visible:rotate-180 group-focus-visible:scale-75 motion-reduce:transition-none"></span>
</button>
<p aria-hidden="true" class="rounded-full px-3.5 py-2 font-tcb28-mono text-[11.5px] text-tcb28-mut ring-1 ring-white/12">2× @property angles · 11s / 7s co-prime · box-shadow starfield</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
/* The one rule utilities cannot express: typed angle properties, so keyframes can rotate a conic gradient. */
@property --tcb28-a {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@property --tcb28-b {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@theme {
--color-tcb28-bg: #1a1328;
--color-tcb28-bg: oklch(0.17 0.03 290);
--color-tcb28-bg2: #0b0911;
--color-tcb28-bg2: oklch(0.1 0.015 280);
--color-tcb28-core: #120e1c;
--color-tcb28-core: oklch(0.14 0.02 290);
--color-tcb28-rim: #8a4fd840;
--color-tcb28-rim: oklch(0.6 0.2 300 / 0.25);
--color-tcb28-glow: #7a3fc799;
--color-tcb28-glow: oklch(0.5 0.2 300 / 0.6);
--color-tcb28-arm-a: #8a4fd859;
--color-tcb28-arm-a: oklch(0.6 0.2 300 / 0.35);
--color-tcb28-arm-b: #5fb0e6;
--color-tcb28-arm-b: oklch(0.72 0.12 225);
--color-tcb28-arm-b2: #5fb0e64d;
--color-tcb28-arm-b2: oklch(0.72 0.12 225 / 0.3);
--color-tcb28-mut: #b5a9cc;
--color-tcb28-mut: oklch(0.76 0.05 295);
--font-tcb28-sans: 'Geist', system-ui, sans-serif;
--font-tcb28-mono: 'Geist Mono', ui-monospace, monospace;
--animate-tcb28-arm-a: tcb28-arm-a 11s linear infinite;
--animate-tcb28-arm-b: tcb28-arm-b 7s linear infinite reverse;
@keyframes tcb28-arm-a {
to {
--tcb28-a: 360deg;
}
}
@keyframes tcb28-arm-b {
to {
--tcb28-b: 360deg;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tcb-28 {
width: 100%;
min-height: 100svh;
}
```How this works
Co-prime periods never sync. 11s and 7s only realign every 77s, so the arms' interference pattern keeps changing — the galaxy looks alive rather than looped.
--animate-tcb28-arm-a: tcb28-arm-a 11s linear infinite;
--animate-tcb28-arm-b: tcb28-arm-b 7s linear infinite reverse;
@keyframes tcb28-arm-a { to { --tcb28-a: 360deg; } }Why change duration, not the animation. Swapping animation-duration keeps the current angle; swapping the animation name would jump the arms back to zero.
Make it yours
- Palette: edit
--color-tcb28-arm-a/arm-b. - Calmer idle:
20s/13speriods. - More stars: append shadow layers — each is
x y 0 spread colour.
Gotchas — read before shipping
- Changing
animation-durationmid-run can jump the angle slightly in some engines; it's barely visible at these speeds. - Without
@propertythe arms are static but the button still works.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
@property landed in Firefox 128, exactly Tailwind v4's floor.