Aurora Background
BackgroundsAmbient gradient field: three soft colour fields drifting at three coprime speeds, a slow hue rotation so the colours themselves move, and a film-grain layer stepped at 12 fps.
Rings expand from a point and fade, one after another, like a pulse. Each ring is an element scaling and fading on its own delay; pure CSS, compositor only. Fills its positioned parent.
Live, from the library itself: scroll, hover, navigate away and back. The demo is never gated.
Adjust · props are custom properties, set inline, so you edit the running instance
JavaScript of its own
0 B
CSS 4.7 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.
Browser support
every browser.
One command adds the integration, the base tokens and every component. Then import what you use.
npx astro add moonarcpnpm astro add moonarcbunx astro add moonarc---
import Ripple from '@moonarc/core/Ripple';
---
<div class="relative grid h-64 place-items-center">
<Ripple color="var(--accent)" />
<button class="relative rounded-full bg-fg px-5 py-3 text-bg">Live</button>
</div>Owns the file, no dependency. The registry item also installs the base tokens.
npx shadcn@latest add https://moonarc.dev/r/ripple.jsonThe CLI needs a components.json and the @/* alias, which Setup has. The file lands in src/components/moonarc/.
The whole component. Self-contained styles in a cascade layer so your classes always win; it imports nothing.
---
/**
* Ripple — rings expand from a point and fade, one after another. Zero JS:
* each ring is an element scaling and fading on its own delay, so the set
* reads as a continuous pulse. Fills its positioned parent.
*/
import type { HTMLAttributes } from 'astro/types';
interface Props extends HTMLAttributes<'div'> {
/** Number of rings in flight. */
count?: number;
/** Seconds a ring takes to expand and fade. */
duration?: number;
/** Ring colour. */
color?: string;
/** Final diameter as a fraction of the box's width, capped at the nearest container's inline size. */
size?: number;
/** Origin, x then y as in background-position: lengths, percentages or keywords; one value centres the other axis. */
origin?: string;
/** Ring thickness in px. */
width?: number;
}
const { count = 4, duration = 4, color = 'currentColor', size = 1.4, origin = '50% 50%', width = 1, class: className, style, ...rest } = Astro.props;
const n = Math.max(1, Math.min(count, 8));
// origin → one left and one top. Written into the inset shorthand it was top and right (inset is top right bottom left),
// so the default 50% 50% put the rings' centre half a ring left of the box's. Split at the top level (a calc() keeps its
// spaces), keywords to percentages, and "top left" read as y x, as background-position reads it.
const parts: string[] = [];
let depth = 0;
let part = '';
for (const ch of origin.trim()) {
if (ch === '(') depth++;
else if (ch === ')') depth--;
if (depth === 0 && /\s/.test(ch)) {
if (part) parts.push(part);
part = '';
} else part += ch;
}
if (part) parts.push(part);
let [x = 'center', y = 'center'] = parts;
if (/^(top|bottom)$/.test(x) || /^(left|right)$/.test(y)) [x, y] = [y, x];
const KEYWORD: Record<string, string> = { left: '0%', top: '0%', center: '50%', right: '100%', bottom: '100%' };
const vars = [`--ma-rp-n:${n};--ma-rp-dur:${duration}s;--ma-rp-color:${color};--ma-rp-size:${size};--ma-rp-x:${KEYWORD[x] ?? x};--ma-rp-y:${KEYWORD[y] ?? y};--ma-rp-w:${width}px`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
---
<div class:list={['ma-ripple', className]} aria-hidden="true" style={vars} {...rest}>
{Array.from({ length: n }, (_, i) => <i style={`--ma-rp-i:${i}`}></i>)}
</div>
<style is:global>
@layer components {
:where(.ma-ripple) {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
contain: strict;
}
:where(.ma-ripple > i) {
position: absolute;
left: var(--ma-rp-x, 50%);
top: var(--ma-rp-y, 50%);
width: calc(min(100%, 100cqi) * var(--ma-rp-size, 1.4));
aspect-ratio: 1;
border: var(--ma-rp-w, 1px) solid var(--ma-rp-color, currentColor);
border-radius: 50%;
translate: -50% -50%;
opacity: 0;
scale: 0;
will-change: scale, opacity;
animation: ma-ripple var(--ma-rp-dur, 4s) var(--ma-ease-out) infinite;
animation-delay: calc(var(--ma-rp-i, 0) * var(--ma-rp-dur, 4s) / var(--ma-rp-n, 4));
}
@media (prefers-reduced-motion: reduce) {
:where(.ma-ripple > i) {
animation: none;
opacity: 0.25;
scale: calc(0.3 + var(--ma-rp-i, 0) * 0.2);
}
}
}
@keyframes ma-ripple {
0% {
opacity: 0.6;
scale: 0;
}
100% {
opacity: 0;
scale: 1;
}
}
</style>| Prop | Type | Default | Description |
|---|---|---|---|
count | number | 4 | Rings in flight, 1–8; evenly spaced over the duration. |
duration | number | 4 | Seconds a ring takes to expand and fade. |
color | string | 'currentColor' | Ring colour. |
size | number | 1.4 | Final diameter as a fraction of the box's width (the positioned parent it fills), capped at the nearest container's inline size. |
origin | string | '50% 50%' | Where the rings start, x then y as in background-position: lengths, percentages or the keywords left, center, right, top and bottom ("top left" works too); one value centres the other axis. The centre of every ring sits on this point. |
width | number | 1 | Ring thickness in px. |
Four static concentric rings at a quarter opacity.
ClientRouterCSS-only; nothing to rebind.
Replaces: Ripple (Magic UI) · pulse rings (custom CSS). See the migration table.
Ambient gradient field: three soft colour fields drifting at three coprime speeds, a slow hue rotation so the colours themselves move, and a film-grain layer stepped at 12 fps.
A field of dots behind content, faded toward the edges, with an optional slow pulse.
An indeterminate arc whose length breathes while it rotates, so it looks like it is working rather than spinning a fixed wedge.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown