liten

Aurora Button

Soft colour clouds drift over a dark field, crossing and mixing like emergent light.

The fill is several radial clouds, each drifting on its own path at its own tempo. Because they cross and overlap, the colour visibly moves and mixes instead of reading as a flat wash. The field is blurred so the clouds melt together, a fractal-noise tile is blended over for real film grain, and all motion stops on reduce.

Installation

Complete the shared Setup first, then copy the component into components/ui/aurora-button.tsx.

components/ui/aurora-button.tsx
'use client';import * as React from 'react';import { cn } from '@/lib/cn';const FOCUS =  'focus-visible:outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:[outline-color:var(--aurora-focus)]';export type AuroraTone = 'aurora' | 'azure' | 'sunset' | 'indigo' | 'magenta' | 'ember';type Blob = {  color: string;  size: number;  x: number;  y: number;  path: 1 | 2 | 3 | 4;};type ToneSpec = {  base: string;  blobs: Blob[];  accent: string;};const TONES: Record<AuroraTone, ToneSpec> = {  aurora: {    base: '#06131f',    accent: '#7fe3c0',    blobs: [      { color: '#7fe6c4', size: 95, x: 46, y: 10, path: 1 },      { color: '#f4a53f', size: 105, x: 54, y: 96, path: 2 },      { color: '#f6e7b6', size: 70, x: 50, y: 58, path: 3 },      { color: '#3aa0ff', size: 80, x: 12, y: 44, path: 4 },    ],  },  azure: {    base: '#1745e6',    accent: '#6ea8ff',    blobs: [      { color: '#eef3ff', size: 100, x: 12, y: 14, path: 1 },      { color: '#74acff', size: 110, x: 44, y: 52, path: 2 },      { color: '#0e2fc4', size: 105, x: 92, y: 64, path: 3 },    ],  },  sunset: {    base: '#0b1130',    accent: '#2b4bff',    blobs: [      { color: '#2b4bff', size: 95, x: 34, y: 50, path: 1 },      { color: '#ff3b2f', size: 100, x: 74, y: 50, path: 2 },      { color: '#ff9a3c', size: 85, x: 98, y: 46, path: 3 },      { color: '#131f66', size: 90, x: 6, y: 50, path: 4 },    ],  },  indigo: {    base: '#140d55',    accent: '#7d3ff5',    blobs: [      { color: '#7d3ff5', size: 100, x: 12, y: 44, path: 1 },      { color: '#4b32d6', size: 105, x: 92, y: 52, path: 2 },      { color: '#b98bff', size: 65, x: 52, y: 22, path: 3 },    ],  },  magenta: {    base: '#2c0b63',    accent: '#ff36c8',    blobs: [      { color: '#ff36c8', size: 105, x: 50, y: 100, path: 1 },      { color: '#7b2ff0', size: 90, x: 22, y: 16, path: 2 },      { color: '#3f16b0', size: 95, x: 92, y: 60, path: 3 },    ],  },  ember: {    base: '#140811',    accent: '#ff9a3c',    blobs: [      { color: '#ff9a3c', size: 100, x: 14, y: 86, path: 1 },      { color: '#c23a2f', size: 105, x: 56, y: 56, path: 2 },      { color: '#7a1f2b', size: 90, x: 90, y: 34, path: 3 },    ],  },};const PATH_TEMPO: Record<Blob['path'], number> = { 1: 1, 2: 1.32, 3: 0.86, 4: 1.14 };export type AuroraButtonProps = {  tone?: AuroraTone;  grain?: boolean;  animate?: boolean;  speed?: number;  accent?: string;  icon?: React.ReactNode;} & React.ButtonHTMLAttributes<HTMLButtonElement>;export function AuroraButton({  tone = 'aurora',  grain = true,  animate = true,  speed = 9,  accent,  icon,  children = 'Start Free Trial',  className,  style,  ...props}: AuroraButtonProps) {  const spec = TONES[tone];  const vars = {    '--aurora-speed': `${speed}s`,    '--aurora-focus': accent ?? spec.accent,  } as React.CSSProperties;  return (    <button      type="button"      style={{ ...vars, ...style }}      className={cn(        'group relative isolate inline-flex cursor-pointer select-none overflow-hidden rounded-[18px]',        'shadow-[0_2px_4px_-1px_rgba(0,0,0,0.18),0_18px_40px_-14px_rgba(0,0,0,0.4)]',        'dark:shadow-[0_2px_6px_-1px_rgba(0,0,0,0.55),0_26px_54px_-18px_rgba(0,0,0,0.75)]',        'transition-transform duration-100 ease-out active:scale-[0.98] motion-reduce:active:scale-100',        FOCUS,        className,      )}      {...props}    >      <span        aria-hidden        className="aurora-field pointer-events-none absolute inset-0 rounded-[inherit]"        style={{ backgroundColor: spec.base }}      >        {spec.blobs.map((b, i) => (          <span            key={i}            data-animate={animate ? '' : undefined}            className="aurora-blob"            style={{              width: `${b.size}%`,              height: `${b.size}%`,              left: `${b.x}%`,              top: `${b.y}%`,              background: `radial-gradient(circle at center, ${b.color} 0%, ${b.color} 12%, transparent 66%)`,              animationName: `aurora-b${b.path}`,              animationDuration: `calc(var(--aurora-speed, 9s) * ${PATH_TEMPO[b.path]})`,            }}          />        ))}      </span>      {grain && (        <span          aria-hidden          className="aurora-grain pointer-events-none absolute inset-0 rounded-[inherit] opacity-[0.22] [mix-blend-mode:overlay]"        />      )}      <span        aria-hidden        className="pointer-events-none absolute inset-0 z-10 rounded-[inherit] shadow-[inset_0_1px_0_0_rgba(255,255,255,0.22),inset_0_0_0_1px_rgba(0,0,0,0.28),inset_0_-14px_30px_-10px_rgba(0,0,0,0.5)]"      />      <span className="relative z-20 flex items-center justify-center gap-2 px-7 py-3.5 text-[16px] font-semibold tracking-[-0.01em] text-white [text-shadow:0_1px_2px_rgba(0,0,0,0.35)]">        {icon != null && (          <span className="flex shrink-0 items-center justify-center [&_svg]:size-[18px]">            {icon}          </span>        )}        {children}      </span>    </button>  );}

Add the animations to your global.css.

global.css
.aurora-field {
  overflow: hidden;
  filter: blur(16px) saturate(1.05);
}
.aurora-blob {
  position: absolute;
  border-radius: 9999px;
  transform: translate(-50%, -50%);
  mix-blend-mode: screen;
  will-change: transform;
}
.aurora-blob[data-animate] {
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
@keyframes aurora-b1 {
  0%,
  100% {
    transform: translate(-50%, -50%) translate(-26%, -20%) scale(1);
  }
  50% {
    transform: translate(-50%, -50%) translate(24%, 22%) scale(1.18);
  }
}
@keyframes aurora-b2 {
  0%,
  100% {
    transform: translate(-50%, -50%) translate(30%, -14%) scale(1.12);
  }
  50% {
    transform: translate(-50%, -50%) translate(-22%, 20%) scale(0.94);
  }
}
@keyframes aurora-b3 {
  0%,
  100% {
    transform: translate(-50%, -50%) translate(-14%, 26%) scale(1.06);
  }
  50% {
    transform: translate(-50%, -50%) translate(18%, -24%) scale(1.22);
  }
}
@keyframes aurora-b4 {
  0%,
  100% {
    transform: translate(-50%, -50%) translate(12%, 12%) scale(1.2);
  }
  50% {
    transform: translate(-50%, -50%) translate(-20%, -10%) scale(1);
  }
}
.aurora-grain {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 140px 140px;
}
@media (prefers-reduced-motion: reduce) {
  .aurora-blob[data-animate] {
    animation: none;
  }
}

Usage

Example.tsx
import { Sparkles } from 'lucide-react';
import { AuroraButton } from '@/components/ui/aurora-button';

export default function Example() {
  return <AuroraButton icon={<Sparkles strokeWidth={2} />}>Start Free Trial</AuroraButton>;
}

Examples

Six colour tones, each with its own drifting clouds.

Turn off drift and grain for a static, flat-lit field.

Props

PropTypeDefaultDescription
toneAuroraTone"aurora"Colour theme of the grainy mesh.
grainbooleantrueOverlay the film-grain texture.
animatebooleantrueDrift the colour clouds like shifting light.
speednumber9Seconds for the base drift cycle.
accentstringtone's hueFocus-outline colour.
iconReact.ReactNode-Optional leading icon.
classNamestring-Forwarded to the button.

tone is one of aurora, azure, sunset, indigo, magenta, or ember. The button also accepts every native <button> prop.

On this page0%