← Backgrounds
Fireflies
<FirefliesBackground /> Fireflies blinking over a dusky meadow.
Your appContent sits on top of the background.
<FirefliesBackground>
{children}
</FirefliesBackground>Props and theme tokens
| Prop | Type | Default | Description |
|---|---|---|---|
theme | Partial<FirefliesTheme> | — | Colour overrides. Keys listed under theme tokens. |
paused | boolean | false | Freezes the animation. |
className | string | — | Applied to the root element. Size it like any block element. |
style | CSSProperties | — | Merged into the root element style, after theme variables. |
children | ReactNode | — | Your content. The background paints behind it. |
| Theme key | CSS variable | Default |
|---|---|---|
skyTop | --sky-top | #0e1a30 |
skyBottom | --sky-bottom | #33284d |
firefly | --firefly | #fbf8c4 |
glow | --glow | #d6f36a |
grass | --grass | #0a1120 |
moon | --moon | #f3ecd2 |
Source files
Copy into components/backgrounds/. The shared files are needed once for all backgrounds.
// <FirefliesBackground>: fireflies wandering and blinking over a dusky meadow,
// with a crescent moon and swaying grass. Animated.
import type { CSSProperties } from 'react';
import { BackgroundFrame, r2, seeded, type BackgroundProps } from './background';
import styles from './fireflies-background.module.css';
export const firefliesTheme = {
skyTop: '#0e1a30',
skyBottom: '#33284d',
firefly: '#fbf8c4',
glow: '#d6f36a',
grass: '#0a1120',
moon: '#f3ecd2',
};
export type FirefliesTheme = typeof firefliesTheme;
const rand = seeded(73);
const offset = (range: number) => `${r2((rand() - 0.5) * range)}px`;
const FIREFLIES = Array.from({ length: 30 }, () => ({
x: r2(3 + rand() * 94),
y: r2(18 + rand() * 72),
wander: r2(10 + rand() * 10),
blink: r2(2.8 + rand() * 3.5),
delay: r2(rand() * 10),
path: { '--ax': offset(80), '--ay': offset(50), '--bx': offset(80), '--by': offset(50), '--cx': offset(80), '--cy': offset(50) },
scale: r2(0.7 + rand() * 0.7),
}));
// Blades are placed at percentage positions so the meadow spans any width without stretching.
const BLADES = Array.from({ length: 70 }, () => ({
x: r2(rand() * 101),
h: r2(28 + rand() * 52),
lean: r2((rand() - 0.5) * 16),
w: r2(2.5 + rand() * 2.5),
sway: r2(2.5 + rand() * 2.5),
delay: r2(rand() * 4),
}));
/** Fireflies wandering and blinking over a dusky meadow. */
export function FirefliesBackground(props: BackgroundProps<FirefliesTheme>) {
return (
<BackgroundFrame
{...props}
defaultTheme={firefliesTheme}
layer={
<>
<div className={styles.sky} />
<svg className={styles.moon} viewBox="0 0 54 54">
<defs>
<mask id="qs-fireflies-moon">
<rect width="54" height="54" fill="#fff" />
<circle cx="36" cy="20" r="21" fill="#000" />
</mask>
</defs>
<circle cx="27" cy="27" r="22" fill="var(--moon)" mask="url(#qs-fireflies-moon)" />
</svg>
{FIREFLIES.map((f, i) => (
<div
key={i}
className={styles.firefly}
style={{ left: `${f.x}%`, top: `${f.y}%`, animationDuration: `${f.wander}s`, animationDelay: `${-f.delay}s`, scale: f.scale, ...f.path } as CSSProperties}
>
<span className={styles.light} style={{ animationDuration: `${f.blink}s`, animationDelay: `${-f.delay}s` }} />
</div>
))}
<svg className={styles.grass}>
{BLADES.map((b, i) => (
<svg key={i} x={`${b.x}%`} y="100%" overflow="visible">
<path
className={styles.blade}
style={{ animationDuration: `${b.sway}s`, animationDelay: `${-b.delay}s` }}
d={`M${-b.w} 0 Q${r2(b.lean * 0.4)} ${r2(-b.h * 0.6)} ${b.lean} ${-b.h} Q${r2(b.lean * 0.4 + 1)} ${r2(-b.h * 0.5)} ${b.w} 0 Z`}
fill="var(--grass)"
/>
</svg>
))}
</svg>
</>
}
/>
);
}
.sky {
position: absolute;
inset: 0;
background: linear-gradient(var(--sky-top), var(--sky-bottom));
}
.moon {
position: absolute;
top: 28px;
right: 36px;
width: 54px;
height: 54px;
filter: drop-shadow(0 0 14px rgb(255 245 210 / 0.35));
}
/* Each firefly wanders a four-point loop and blinks on its own rhythm. */
.firefly {
position: absolute;
animation: wander ease-in-out infinite;
}
.light {
display: block;
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--firefly);
box-shadow: 0 0 6px 2px var(--glow), 0 0 18px 6px color-mix(in srgb, var(--glow) 45%, transparent);
animation: blink ease-in-out infinite;
}
.grass {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 90px;
overflow: visible;
}
.blade {
animation: sway ease-in-out infinite alternate;
}
@keyframes wander {
0%, 100% { transform: translate(0, 0); }
25% { transform: translate(var(--ax), var(--ay)); }
50% { transform: translate(var(--bx), var(--by)); }
75% { transform: translate(var(--cx), var(--cy)); }
}
@keyframes blink {
0%, 35%, 100% { opacity: 0.12; }
50% { opacity: 1; }
65% { opacity: 0.3; }
}
@keyframes sway {
from { transform: rotate(-4deg); }
to { transform: rotate(4deg); }
}
// Shared frame for every background: theming, layering and pausing.
// Backgrounds are pure server components: no client JavaScript.
import type { CSSProperties, ReactNode } from 'react';
import styles from './background.module.css';
export { styles as backgroundStyles };
/** Deterministic PRNG so particles render identically on server and client. */
export function seeded(seed: number): () => number {
return () => {
seed = (seed + 0x6d2b79f5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
/** Round for stable, compact inline styles. */
export const r2 = (n: number) => Math.round(n * 100) / 100;
/** Props every background accepts. `T` is the background's theme (colour tokens). */
export type BackgroundProps<T> = {
/** Override any of the background's colours. */
theme?: Partial<T>;
/** Freeze animated backgrounds. Static backgrounds ignore it. */
paused?: boolean;
className?: string;
style?: CSSProperties;
/** Your content. The background paints behind it. */
children?: ReactNode;
};
// baseColor → --base-color, blob1 → --blob-1
const toVar = (key: string) => `--${key.replace(/([A-Z])/g, '-$1').replace(/(\d+)/g, '-$1').toLowerCase()}`;
type BackgroundFrameProps<T extends Record<string, string>> = BackgroundProps<T> & {
defaultTheme: T;
/** The texture itself, rendered in an absolutely positioned layer behind the children. */
layer: ReactNode;
};
export function BackgroundFrame<T extends Record<string, string>>({
defaultTheme,
layer,
theme,
paused,
className,
style,
children,
}: BackgroundFrameProps<T>) {
const vars: Record<string, string> = {};
for (const [key, value] of Object.entries({ ...defaultTheme, ...theme })) {
if (value) vars[toVar(key)] = value;
}
const classes = [styles.root, paused && styles.paused, className].filter(Boolean).join(' ');
return (
<div className={classes} style={{ ...vars, ...style } as CSSProperties}>
<div className={styles.layer} aria-hidden="true">
{layer}
</div>
{children}
</div>
);
}
/* The root establishes a stacking context so the layer sits behind children
without escaping behind the page. */
.root {
position: relative;
isolation: isolate;
}
.layer {
position: absolute;
inset: 0;
z-index: -1;
overflow: hidden;
pointer-events: none;
/* Lets particles move by the layer's own size: 100cqh is its full height, 100cqw its width. */
container-type: size;
}
/* A particle starts just above the layer at a random horizontal position. */
.particle {
position: absolute;
top: 0;
}
.paused,
.paused * { animation-play-state: paused !important; }
@media (prefers-reduced-motion: reduce) {
.layer,
.layer * { animation: none !important; }
/* Without motion, particles rest at a scattered position instead of the top edge. */
.particle { top: var(--y, 0); }
}