Aurora
Soft veils of light in the accent color that drift slowly. Quiet in light mode, luminous in dark mode.
One of 6 components in Backgrounds
Variants
Details
What Aurora brings along, read from the registry and the source.
- Category
- Backgrounds
- Area
- Elements
- Variants
- 3
- Version
- 1.0.0 · since 1.0.0
- File
- components/ui/aurora.tsx
- Brings along
- utilskit
- npm packages
- None
- Languages
- Texts come through props
- Studio
- Adjust in code
- Build with AI
- Over MCP in Claude, Cursor, Copilot and Codex
Props
The AuroraProps interface from components/ui/aurora.tsx, with the comments from the code.
intensityoptionalAuroraIntensitysoftmediumvivid
How strong the veils are. Light mode always stays quieter than dark.
placementoptionalAuroraPlacementtopcenterbottom
Where the light hangs: from the top edge, behind the middle or rising from the bottom
speedoptionalnumberSeconds for one slow sway of the veils, default 22
parallaxoptionalnumberPixels the veils drift against the page over a full scroll pass, a quiet depth. Default 36, 0 is off.
Accessibility
Read from the source of the component.
- The server renders the starting state in full, JavaScript only adds the interaction.
Motion
- Moves with the scroll, its progress follows the position on the page.
- Brings animations of its own, calmly timed and without wobble.
- With reduced motion (prefers-reduced-motion) it stays still.
Source
The file aiw add puts into your project. From then on it is yours.
Show the file, 174 lines
'use client';
import { useEffect, useRef } from 'react';
import { cn, prefersReducedMotion } from '@/lib/kit';
export type AuroraIntensity = 'soft' | 'medium' | 'vivid';
export type AuroraPlacement = 'top' | 'center' | 'bottom';
export interface AuroraProps {
/** How strong the veils are. Light mode always stays quieter than dark. */
intensity?: AuroraIntensity;
/** Where the light hangs: from the top edge, behind the middle or rising from the bottom */
placement?: AuroraPlacement;
/** Seconds for one slow sway of the veils, default 22 */
speed?: number;
/** Pixels the veils drift against the page over a full scroll pass, a quiet depth. Default 36, 0 is off. */
parallax?: number;
className?: string;
}
const CSS = `
@keyframes kitfx-aurora-sway {
0%, 100% { transform: translate3d(-5%, 0, 0) rotate(var(--kitfx-r, 0deg)) scaleY(1); }
50% { transform: translate3d(5%, var(--kitfx-y, 8%), 0) rotate(calc(var(--kitfx-r, 0deg) * -0.6)) scaleY(1.25); }
}
@keyframes kitfx-aurora-breathe { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; } }
.kitfx-aurora * { animation-play-state: var(--kitfx-play, running); }
@media (prefers-reduced-motion: reduce) { .kitfx-aurora * { animation: none !important; } .kitfx-aurora-depth { transform: none !important; } }
/* Tones. Fallback without relative colors: thin mixes of the accent */
.kitfx-aurora {
--kitfx-a1: color-mix(in oklab, var(--primary) 22%, transparent);
--kitfx-a2: color-mix(in oklab, var(--primary) 11%, transparent);
--kitfx-a3: color-mix(in oklab, var(--primary) 16%, transparent);
}
.dark .kitfx-aurora {
--kitfx-a1: color-mix(in oklab, var(--primary) 50%, transparent);
--kitfx-a2: color-mix(in oklab, var(--primary) 26%, transparent);
--kitfx-a3: color-mix(in oklch, color-mix(in oklab, var(--primary) 70%, var(--foreground)) 36%, transparent);
}
/*
* --kitfx-n says how neutral the accent is: 1 for the grey default, 0 for any
* real color. Grey veils on white look like smudges, so a neutral accent turns
* into a pearl light: mid lightness, a breath of chroma, and the veils fan out
* in hue (lavender, rose, sky). A colored accent keeps its hue and strength.
*/
@supports (color: oklch(from red l c h)) {
.kitfx-aurora {
--kitfx-n: clamp(0, 1 - c / 0.06, 1);
--kitfx-a1: oklch(from var(--primary) calc(l + (0.72 - l) * var(--kitfx-n)) max(c, 0.08) h / calc(0.22 + 0.16 * var(--kitfx-n)));
--kitfx-a2: oklch(from var(--primary) calc(l + (0.78 - l) * var(--kitfx-n)) max(c, 0.075) calc(h + 60 * var(--kitfx-n)) / calc(0.11 + 0.2 * var(--kitfx-n)));
--kitfx-a3: oklch(from var(--primary) calc(l + (0.78 - l) * var(--kitfx-n)) max(c, 0.07) calc(h - 78 * var(--kitfx-n)) / calc(0.16 + 0.17 * var(--kitfx-n)));
}
.dark .kitfx-aurora {
--kitfx-a1: oklch(from var(--primary) calc(l + (0.8 - l) * var(--kitfx-n)) max(c, 0.07) h / 0.5);
--kitfx-a2: oklch(from var(--primary) calc(l + (0.76 - l) * var(--kitfx-n)) max(c, 0.07) calc(h + 60 * var(--kitfx-n)) / calc(0.26 + 0.08 * var(--kitfx-n)));
--kitfx-a3: oklch(from var(--primary) calc(l + (0.88 - l) * 0.45) max(c * 0.75, 0.06) calc(h - 78 * var(--kitfx-n)) / 0.38);
}
}
/* In dark mode the veils add light instead of laying a grey film over the page */
.dark .kitfx-aurora { mix-blend-mode: plus-lighter; }
`;
/* Peak strength per intensity, light and dark. Light stays a sheen, never a cloud. */
const INTENSITY: Record<AuroraIntensity, string> = {
soft: 'opacity-[0.55] dark:opacity-[0.55]',
medium: 'opacity-[0.8] dark:opacity-[0.78]',
vivid: 'opacity-[0.9] dark:opacity-[0.88]',
};
interface Ribbon { at: number; h: number; w: number; x: number; r: number; tone: 'a1' | 'a2' | 'a3'; blur: number; t: number; y: number }
/* The veils, measured from the anchored edge (top, middle or bottom) */
const RIBBONS: Ribbon[] = [
{ at: 4, h: 34, w: 120, x: -14, r: -7, tone: 'a2', blur: 46, t: 1, y: 10 },
{ at: 16, h: 18, w: 96, x: -4, r: -4, tone: 'a1', blur: 30, t: 1.35, y: -8 },
{ at: 10, h: 26, w: 80, x: 34, r: 6, tone: 'a3', blur: 40, t: 1.7, y: 12 },
{ at: 26, h: 10, w: 70, x: 10, r: -3, tone: 'a1', blur: 18, t: 1.15, y: 6 },
];
/* Fine vertical rays, like the curtain of a real aurora */
const RAYS = 'repeating-linear-gradient(90deg, #000 0px, rgb(0 0 0 / 0.66) 13px, #000 27px, rgb(0 0 0 / 0.8) 38px, #000 50px, rgb(0 0 0 / 0.58) 61px, #000 74px)';
const EDGE: Record<AuroraPlacement, string> = {
top: 'radial-gradient(ellipse 80% 90% at 50% 0%, #000 30%, transparent 78%)',
center: 'radial-gradient(ellipse 72% 52% at 50% 50%, #000 40%, transparent 100%), radial-gradient(ellipse 34% 30% at 50% 50%, rgb(0 0 0 / 0.22), #000)',
bottom: 'radial-gradient(ellipse 80% 90% at 50% 100%, #000 30%, transparent 78%)',
};
/**
* Scroll pass. While `on`, writes --kitfx-p on the element: 0 when its top
* meets the bottom of the window, 1 when its bottom leaves at the top. Never
* with reduced motion, the CSS then falls back to the resting value.
*/
function useScrollPass(ref: { current: HTMLElement | null }, on: boolean) {
useEffect(() => {
const el = ref.current;
if (!el || !on || prefersReducedMotion()) return;
let frame = 0;
const update = () => {
frame = 0;
const r = el.getBoundingClientRect();
const p = Math.min(1, Math.max(0, (window.innerHeight - r.top) / (r.height + window.innerHeight)));
el.style.setProperty('--kitfx-p', p.toFixed(4));
};
const onScroll = () => {
if (!frame) frame = requestAnimationFrame(update);
};
update();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
return () => {
cancelAnimationFrame(frame);
window.removeEventListener('scroll', onScroll);
window.removeEventListener('resize', onScroll);
el.style.removeProperty('--kitfx-p');
};
}, [ref, on]);
}
/**
* Soft moving veils of light in the accent color. Fills its parent
* (give the parent `relative isolate overflow-hidden`, content after it with
* `relative`). Decorative, pauses out of view, still image with reduced motion.
*/
export function Aurora({ intensity = 'medium', placement = 'top', speed = 22, parallax = 36, className }: AuroraProps) {
const ref = useRef<HTMLDivElement>(null);
useScrollPass(ref, parallax !== 0);
useEffect(() => {
const el = ref.current;
if (!el || typeof IntersectionObserver === 'undefined') return;
const io = new IntersectionObserver(([e]) => el.style.setProperty('--kitfx-play', e.isIntersecting ? 'running' : 'paused'));
io.observe(el);
return () => io.disconnect();
}, []);
const edge = EDGE[placement];
return (
<div
ref={ref}
aria-hidden
data-clip-ok
className={cn('kitfx-aurora pointer-events-none absolute inset-0 overflow-hidden', INTENSITY[intensity], placement === 'center' && 'max-sm:opacity-[0.6] max-sm:dark:opacity-[0.55]', className)}
style={{ maskImage: edge, WebkitMaskImage: edge, maskComposite: 'intersect', WebkitMaskComposite: 'source-in' }}
>
<style dangerouslySetInnerHTML={{ __html: CSS }} />
{/* The veils sit a little deeper than the page: they drift against the scroll */}
<div className="kitfx-aurora-depth absolute inset-0" style={parallax ? { transform: `translate3d(0, calc((var(--kitfx-p, 0.5) - 0.5) * ${parallax}px), 0)` } : undefined}>
<div className="absolute inset-x-[-10%] inset-y-0" style={{ maskImage: RAYS, WebkitMaskImage: RAYS, transform: 'skewX(-12deg)' }}>
{RIBBONS.map((r, i) => {
const at = placement === 'center' ? { top: `${18 + r.at * 1.3}%` } : placement === 'bottom' ? { bottom: `${r.at}%` } : { top: `${r.at}%` };
return (
<div
key={i}
className="absolute rounded-[50%]"
style={{
...at,
left: `${r.x}%`,
width: `${r.w}%`,
height: `${r.h}%`,
background: `linear-gradient(90deg, transparent, var(--kitfx-${r.tone}) 28%, var(--kitfx-${r.tone}) 62%, transparent)`,
filter: `blur(${r.blur}px)`,
['--kitfx-r' as string]: `${r.r}deg`,
['--kitfx-y' as string]: `${placement === 'bottom' ? -r.y : r.y}%`,
transform: `rotate(${r.r}deg)`,
animation: `kitfx-aurora-sway ${Math.round(speed * r.t)}s ease-in-out ${-i * 5}s infinite, kitfx-aurora-breathe ${Math.round(speed * r.t * 0.8)}s ease-in-out ${-i * 3}s infinite`,
}}
/>
);
})}
</div>
</div>
</div>
);
}Frequent questions
How do I install Aurora?
With aiw add @ai-website/aurora. The command puts components/ui/aurora.tsx into your project, the code is yours from then on. It needs no further npm packages.
Which variants does Aurora have?
3 variants, Soft from the top, Vivid in the middle and Horizon.
Can an AI assistant build with Aurora?
Yes. Through the MCP server of the aiw CLI, Claude, Cursor, GitHub Copilot, Windsurf, Codex and Gemini find Aurora with aiw_components and install it with aiw_add @ai-website/aurora.