Scroll Reveal
ScrollEntrance on scroll that plays once and stays, staggers its children, scrubs with the scroll position when asked, honours reduced motion, and keeps working after every ClientRouter navigation.
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. No WebGL, no canvas, no blur filter: every layer is a transform on the compositor.
Ambient.
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 6.5 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 Aurora from '@moonarc/core/Aurora';
---
<section class="relative">
<Aurora />
<h1 class="relative">Content above the field</h1>
</section>Owns the file, no dependency. The registry item also installs the base tokens.
npx shadcn@latest add https://moonarc.dev/r/aurora.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.
---
/**
* Aurora — ambient gradient field, zero JS, no WebGL.
*
* Four layers, each on the compositor: three soft colour fields drifting at
* three coprime periods so the pattern never repeats, a slow hue rotation on
* the whole field so the colours themselves move, and a grain layer stepped
* at 12 fps so the surface reads as film, not vector. No filter: blur() — the
* softness comes from the gradient falloff itself, so every frame costs a
* transform (and one accelerated hue-rotate), never a re-blur. Fills its
* positioned parent.
*/
import type { HTMLAttributes } from 'astro/types';
interface Props extends HTMLAttributes<'div'> {
/** Three colours, any CSS colour. */
colors?: [string, string, string];
/** Seconds per drift cycle of the slowest field; the others run at 0.72× and 0.55×. */
duration?: number;
/** Overall strength, 0–1. */
opacity?: number;
/** Degrees the hues wander either side of the given colours; 0 pins them. */
hue?: number;
/** Grain strength, 0–1; 0 removes the layer. */
grain?: number;
}
const {
colors = ['oklch(70% 0.19 300)', 'oklch(78% 0.15 200)', 'oklch(75% 0.17 350)'],
duration = 22,
opacity = 0.55,
hue = 40,
grain = 0.5,
class: className,
style,
...rest
} = Astro.props;
const vars = [
`--ma-aurora-1:${colors[0]}`,
`--ma-aurora-2:${colors[1]}`,
`--ma-aurora-3:${colors[2]}`,
`--ma-aurora-duration:${duration}s`,
`--ma-aurora-opacity:${opacity}`,
`--ma-aurora-hue:${hue}deg`,
`--ma-aurora-grain:${grain}`,
typeof style === 'string' ? style : '',
]
.filter(Boolean)
.join(';');
---
<div
class:list={['ma-aurora', className]}
aria-hidden="true"
data-grain={grain > 0 ? '' : undefined}
style={vars}
{...rest}
>
<span class="ma-aurora__field">
<span class="ma-aurora__blob"></span>
<span class="ma-aurora__blob"></span>
<span class="ma-aurora__blob"></span>
</span>
</div>
<style is:global>
@layer components {
:where(.ma-aurora) {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
opacity: var(--ma-aurora-opacity, 0.55);
contain: strict;
}
:where(.ma-aurora__field) {
position: absolute;
inset: -10%;
will-change: filter;
animation: ma-aurora-hue calc(var(--ma-aurora-duration, 22s) * 2.6) ease-in-out infinite alternate;
}
:where(.ma-aurora__blob) {
position: absolute;
width: 70%;
aspect-ratio: 1;
border-radius: 50%;
background: radial-gradient(circle at center, var(--ma-aurora-c) 0%, color-mix(in oklch, var(--ma-aurora-c), transparent 55%) 28%, transparent 64%);
will-change: translate, scale;
animation: ma-aurora-drift var(--ma-aurora-duration, 22s) ease-in-out infinite alternate;
}
:where(.ma-aurora__blob:nth-child(1)) {
--ma-aurora-c: var(--ma-aurora-1);
top: -25%;
left: -15%;
}
:where(.ma-aurora__blob:nth-child(2)) {
--ma-aurora-c: var(--ma-aurora-2);
top: 5%;
right: -20%;
animation-name: ma-aurora-drift-2;
animation-duration: calc(var(--ma-aurora-duration, 22s) * 0.72);
animation-delay: calc(var(--ma-aurora-duration, 22s) / -3);
}
:where(.ma-aurora__blob:nth-child(3)) {
--ma-aurora-c: var(--ma-aurora-3);
bottom: -35%;
left: 15%;
animation-name: ma-aurora-drift-3;
animation-duration: calc(var(--ma-aurora-duration, 22s) * 0.55);
animation-delay: calc(var(--ma-aurora-duration, 22s) / -1.5);
}
/* grain: a tiled SVG turbulence, stepped so it flickers like film at 12 fps; translate only, so it stays on the compositor */
:where(.ma-aurora[data-grain])::after {
content: '';
position: absolute;
inset: -20%;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.5 0 0 0 0 0.5 0 0 0 0 0.5 0 0 0 1 0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E");
background-size: 200px 200px;
mix-blend-mode: overlay;
opacity: calc(var(--ma-aurora-grain, 0.5) * 0.5);
animation: ma-aurora-grain 0.5s steps(6) infinite;
}
@media (prefers-reduced-motion: reduce) {
:where(.ma-aurora__field),
:where(.ma-aurora__blob),
:where(.ma-aurora[data-grain])::after {
animation: none;
will-change: auto;
}
}
}
@keyframes ma-aurora-drift {
0% {
translate: 0 0;
scale: 1;
}
50% {
translate: 28% 14%;
scale: 1.25;
}
100% {
translate: -14% 30%;
scale: 0.9;
}
}
@keyframes ma-aurora-drift-2 {
0% {
translate: 0 0;
scale: 1.1;
}
50% {
translate: -30% 20%;
scale: 0.85;
}
100% {
translate: -10% -18%;
scale: 1.2;
}
}
@keyframes ma-aurora-drift-3 {
0% {
translate: 0 0;
scale: 0.95;
}
50% {
translate: 24% -30%;
scale: 1.2;
}
100% {
translate: -20% -8%;
scale: 1;
}
}
@keyframes ma-aurora-hue {
from {
filter: hue-rotate(calc(var(--ma-aurora-hue, 40deg) * -1));
}
to {
filter: hue-rotate(var(--ma-aurora-hue, 40deg));
}
}
@keyframes ma-aurora-grain {
to {
translate: 6% 4%;
}
}
</style>| Prop | Type | Default | Description |
|---|---|---|---|
colors | [string, string, string] | violet, cyan, rose (oklch) | Three colours, any CSS colour. Keep chroma moderate on light backgrounds. |
duration | number | 22 | Seconds per drift cycle of the slowest field; the others run at 0.72× and 0.55× so the pattern never repeats. Under 12 stops being ambient. |
hue | number | 40 | Degrees the hues wander either side of the given colours over 2.6 cycles; 0 pins them. |
grain | number | 0.5 | Grain strength, 0–1; 0 removes the layer. |
opacity | number | 0.55 | Overall strength, 0–1. |
The fields stop drifting, the hue stops turning and the grain holds still: a static gradient with texture.
ClientRouterCSS-only; nothing to rebind.
Replaces: Aurora (React Bits, OGL) · AuroraBackground (Aceternity) · BackgroundGradientAnimation (Aceternity). See the migration table.
Entrance on scroll that plays once and stays, staggers its children, scrubs with the scroll position when asked, honours reduced motion, and keeps working after every ClientRouter navigation.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown