A slow-drifting night sky with twinkling stars, for space, launch and AI product heroes.
Look up
Customize
Usage
import { StarField } from "@/components/motionkit/star-field";
<StarField className="h-96 rounded-3xl">
<h1>Your hero</h1>
</StarField>Motion
The exact curves this component runs, plotted with Motion's own maths.
Drift
Runs on its own
Twinkle
Runs on its own
Works in the App Router as is. Requires Tailwind CSS v4.
npm install motionSave as components/motionkit/star-field.tsx.
"use client";
import type { ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";
type StarFieldProps = {
children?: ReactNode;
/** Number of stars. */
count?: number;
/** Seconds for the field to drift one full screen upward. */
duration?: number;
/** Star colour. */
color?: string;
className?: string;
};
// integer hash, so star positions match on server and client in every browser
function rand(seed: number) {
let t = Math.imul(seed ^ (seed >>> 15), 0x2c1b3c6d);
t = Math.imul(t ^ (t >>> 12), 0x297a2d39);
return ((t ^ (t >>> 15)) >>> 0) / 4294967296;
}
export function StarField({ children, count = 90, duration = 80, color = "#F6F1E8", className = "" }: StarFieldProps) {
const reduceMotion = useReducedMotion();
const stars = Array.from({ length: count }, (_, index) => ({
x: rand(index * 3 + 1) * 100,
y: rand(index * 3 + 2) * 100,
size: 1 + rand(index * 3 + 3) * 1.6,
// only about a third of the stars twinkle, which reads as natural and keeps the work small
twinkle: index % 3 === 0,
}));
return (
<div className={`relative isolate overflow-hidden bg-[#0C1010] ${className}`}>
{/* two stacked copies of the same field, scrolled by exactly one copy's height, loop seamlessly */}
<motion.div
aria-hidden
className="absolute inset-x-0 top-0 -z-10 h-[200%]"
animate={reduceMotion ? undefined : { y: ["0%", "-50%"] }}
transition={{ duration, repeat: Infinity, ease: "linear" }}
>
{[0, 50].map((offset) =>
stars.map((star, index) => (
<motion.span
key={`${offset}-${index}`}
className="absolute rounded-full"
style={{
left: `${star.x}%`,
top: `${offset + star.y / 2}%`,
width: star.size,
height: star.size,
backgroundColor: color,
opacity: 0.35 + (star.size - 1) * 0.3,
boxShadow: star.size > 2.2 ? `0 0 6px ${color}` : undefined,
}}
animate={!reduceMotion && star.twinkle ? { opacity: [0.2, 1, 0.2] } : undefined}
transition={{ duration: 2 + rand(index + 7) * 3, delay: rand(index + 11) * 4, repeat: Infinity, ease: "easeInOut" }}
/>
)),
)}
</motion.div>
{/* a faint glow at the bottom gives the field some depth */}
<div aria-hidden className="absolute inset-0 -z-10 bg-[radial-gradient(ellipse_at_bottom,rgb(14_124_134/0.18),transparent_60%)]" />
{children}
</div>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
| count | number | 90 | Number of stars. |
| duration | number | 80 | Seconds for the field to drift one full screen upward. |
| color | string | "#F6F1E8" | Star colour. |
| children | ReactNode | — | Content rendered above the effect. |
Make a wish
Meteors
Shooting stars streaking across a dotted sky at scattered times.
Ambient by default
Aurora Background
Drifting, blurred color fields for hero sections and empty states.
Warm, textured, alive
Grain Gradient
Slow, drifting colour with a fine film grain, for warm editorial heroes.