Constell is a self-contained, drop-in animated background component for any webpage. It paints a living, ever-shifting night sky — dozens of stars drifting at different depths, multi-toned nebula clouds swaying behind them, parallax shift that follows the cursor, and the occasional shooting star streaking across — onto a canvas element positioned behind your page content.
The component ships as a tiny, dependency-free pair of files placed beside your HTML:
constell.js— an ES module that performs all of the rendering and animation, exposes a smallConstellAPI (init/configure) and is documented in detail further down this README.constell.css— an optional stylesheet that provides sensible dark-mode defaults for the page's background gradient and overlay, plus a set of CSS custom properties you can override to retheme the look without touching any JavaScript.
There is no build step, no framework, no runtime dependency and no network request. Drop the two
files into a project, add a canvas element, and call Constell.init() — your static page becomes
a calm, animated, deep-space surface in a handful of lines.
<!-- 1. Include CSS (optional; ships a pleasant default) -->
<link rel="stylesheet" href="constell.css" />
<!-- 2. Place a canvas element -->
<canvas id="scene"></canvas>
<!-- 3. Initialise with defaults or custom config -->
<script type="module">
import { Constell } from "./constell.js";
// Defaults only
Constell.init("#scene");
// Or with overrides
Constell.init("#scene", {
star: { count: 60, hueMin: 170, hueMax: 290 },
motion: { frameMs: 33 },
parallax: { sensitivityX: 0.0008 },
});
</script>You can also call Constell.configure(overrides) at any time to change settings after init — stars are re-seeded when star.count changes.
Follow these steps to add the constellation background to one of your own pages. The whole process takes only a few minutes and the result is a small handful of files plus a couple of lines of HTML.
Copy constell.js and constell.css into the same folder as the HTML page you want to
enhance (or anywhere convenient — just adjust the paths in the steps below). No other files,
bundlers or libraries are required.
Add the stylesheet inside the <head> of your page:
<link rel="stylesheet" href="constell.css" />This gives you a dark body gradient, sets up basic full-viewport geometry, and exposes the
documented --constell-* theming variables. The stylesheet is optional — if you already
have your own background styles, feel free to skip this step.
Place a <canvas> somewhere in your <body>. Its id will be used to attach the animation:
<canvas id="scene"></canvas>When constell.css is loaded, that element is automatically positioned as a fixed,
full-viewport background layer behind your content (via the #scene rule). If you skipped
step 2, add the equivalent position: fixed; inset: 0; width: 100%; height: 100%; styles yourself
so the canvas sits behind everything else.
At the end of your page (or in your own JS module), import and call Constell.init():
<script type="module">
import { Constell } from "./constell.js";
// Defaults — 42 stars, nebulae, parallax, occasional shooting stars
Constell.init("#scene");
</script>You can pass either a CSS-selector string (such as "#scene") or the canvas element itself.
With these four lines you already have a working animated background.
Pass a config object as the second argument to override the defaults at startup:
<script type="module">
import { Constell } from "./constell.js";
Constell.init("#scene", {
star: { count: 90 },
motion: { speed: 6 }, // faster overall motion
shootingStar: { chancePerFrame: 0.02 }, // more meteors
});
</script>Or change settings later at runtime from anywhere in your code:
Constell.configure({ star: { count: 120 } }); // stars are re-seeded automaticallyFor purely visual changes (background colours, blur strength, text colours, etc.) you can override
the documented --constell-* CSS custom properties on :root without touching any JavaScript.
Browsers refuse to load ES modules from raw file:// URLs, so you need to serve your page over
HTTP. Any static server works — for example:
python -m http.server 7070 --directory .
# then open http://localhost:7070Other common options: npx serve ., VS Code's Live Server extension, or whatever workflow
you normally use for static sites. Hit reload after editing and the animated background updates
immediately.
All values are optional; missing keys fall back to the defaults shown below.
| Key | Default | Description |
|---|---|---|
count |
42 |
Total number of stars |
depthMin |
0.18 |
Minimum depth (affects size, brightness, drift speed) |
depthMax |
1.0 |
Maximum depth |
radiusMin |
0.45 |
Minimum star radius (px at full depth) |
radiusMax |
2.1 |
Maximum star radius |
speedXMin |
-0.004 |
Minimum horizontal drift speed (normalized/s) |
speedXMax |
0.004 |
Maximum horizontal drift speed |
speedYMin |
-0.003 |
Minimum vertical drift speed |
speedYMax |
0.003 |
Maximum vertical drift speed |
twinkleSpeedMin |
0.5 |
Minimum twinkle oscillation speed (rad/s) |
twinkleSpeedMax |
2.2 |
Maximum twinkle oscillation speed |
hueMin |
198 |
Minimum hue (degrees, HSL) |
hueMax |
236 |
Maximum hue (degrees, HSL) |
| Key | Default | Description |
|---|---|---|
frameMs |
100 |
Minimum ms between frames (set to 33 for ~30 fps) |
pointerLerp |
0.07 |
Smoothness of pointer tracking (0 = none, 1 = instant) |
prefersReducedMotion |
false |
Force no animation (stars still twinkle). Pass true to freeze all motion. |
speed |
5 |
Master speed multiplier for all motion (1 = very slow, 5 = normal, 10 = 2× speed). No clamping — higher values work too. |
The speed value scales star drift/parallax, nebula animation timing, and shooting-star movement/lifespan proportionally. Values below 5 slow everything down; above 5 accelerate it.
| Key | Default | Description |
|---|---|---|
sensitivityX |
0.0006 |
Horizontal drift multiplier per pointer unit (scaled by 1 - depth) |
sensitivityY |
0.00045 |
Vertical drift multiplier |
depthFactorX |
0.05 |
Non-depth parallax offset factor (screen space, X) |
depthFactorY |
0.04 |
Non-depth parallax offset factor (screen space, Y) |
| Key | Default | Description |
|---|---|---|
driftBase |
0.14 |
Base drift scale applied to all stars |
driftDepthScale |
0.9 |
Additional drift per depth unit |
| Key | Default | Description |
|---|---|---|
alphaBase |
0.18 |
Base star opacity |
alphaDepthScale |
0.55 |
Opacity added per depth unit (max ≈ 0.73) |
glowThreshold |
0.65 |
Depth above which a radial glow is drawn |
glowAlphaMultiplier |
0.55 |
Glow opacity relative to star opacity |
glowRadiusMultiplier |
5 |
Glow radius = star.radius * multiplier |
twinkleAmplitude |
0.16 |
Sinusoidal brightness variation range (± 16 %) |
An array of up to 3 layer objects. Each accepts the keys below:
| Key | Default | Description |
|---|---|---|
xBase |
varies | Normalised X position (0–1, fraction of canvas width) |
yBase |
varies | Normalised Y position (0–1) |
radiusScale |
varies | Radius = max(canvasWidth, canvasHeight) * radiusScale |
color |
varies | Radial gradient colour (e.g. "rgba(126,94,255,0.18)") |
driftX.enabled |
false | X position animated via sin(t*freq) drift offset |
driftX.freq |
varies | Oscillation frequency (rad/s) |
driftX.amount |
varies | Drift amplitude as fraction of canvas width |
driftX.invert |
false | Negate the X drift offset (layer 2 style) |
driftY.enabled |
false | Y position animated via sin(t*freq) drift offset |
driftY.freq |
varies | Oscillation frequency (rad/s), defaults to driftX.freq |
driftY.scale |
1 | Vertical drift scaling factor |
driftY.amount |
varies | Drift amplitude as fraction of canvas height |
animatedX.enabled |
false | Pure sin(t*freq)*width*amount offset (no drift) |
animatedY.enabled |
false | Pure cos/sin(t*freq)*height*amount offset |
animatedY.type |
"sin" |
Oscillator — either "sin" or "cos" |
parallaxX |
0 |
Pointer X parallax multiplier |
parallaxY |
0 |
Pointer Y parallax multiplier |
A shooting star spawns randomly at a rate controlled by chancePerFrame. Multiple can be active simultaneously (max 2). Each has a bright head with a fading trail.
| Key | Default | Description |
|---|---|---|
enabled |
true |
Enable/disable shooting stars entirely |
chancePerFrame |
0.008 |
Spawn probability per frame (≈1 every 2s at 60 fps, ≈1 every 4s at 30 fps) |
speedMin |
4 |
Min speed in pixels/frame |
speedMax |
10 |
Max speed in pixels/frame |
angleMin |
25 |
Min sweep angle from horizontal in degrees (diagonal streaks) |
angleMax |
55 |
Max sweep angle in degrees |
lengthMin |
80 |
Minimum trail length in px |
lengthMax |
200 |
Maximum trail length in px |
thicknessMin |
1.8 |
Minimum head thickness in px |
thicknessMax |
3.2 |
Maximum head thickness in px |
hueMin |
190 |
Minimum hue for star color (blue-white range) |
hueMax |
240 |
Maximum hue |
headAlpha |
0.95 |
Peak brightness of the head |
fadeInFrames |
3 |
Frames to reach full brightness on spawn |
lifetimeMin |
25 |
Minimum lifespan in frames |
lifetimeMax |
60 |
Maximum lifespan in frames |
Example — frequent colorful meteors:
Constell.configure({
shootingStar: {
chancePerFrame: 0.03,
hueMin: 280, // purples
hueMax: 340,
speedMin: 6,
lengthMax: 300,
},
});Example — disable entirely:
Constell.configure({ shootingStar: { enabled: false } });| Key | Default | Description |
|---|---|---|
colorTop |
#030510 |
Gradient start & end colour (dark corners) |
colorMid |
#061425 |
Mid-stop colour (midStop position) |
midStop |
0.55 |
Normalised position of the mid-colour stop |
The motion.speed setting (default 5, soft range 1–10) uniformly scales every moving thing: star drift, nebula animation timing, and shooting-star travel speed and lifetime.
// Half speed — lazy stars, slow nebulae
Constell.configure({ motion: { speed: 2.5 } });
// Double pace — energetic everything
Constell.configure({ motion: { speed: 10 } });These override styling for the demo page overlay and body gradient background. They do not affect canvas rendering (that is controlled by JS config above).
| Variable | Default | Controls |
|---|---|---|
--constell-bg-start |
#040816 |
Body gradient start |
--constell-bg-end |
#030510 |
Body gradient end |
--constell-nebula-a |
rgba(122,92,255,0.22) |
CSS radial nebula overlay A |
--constell-nebula-b |
rgba(0,217,255,0.16) |
CSS radial nebula overlay B |
--constell-overlay-bg |
rgba(4,8,22,0.46) |
Info card background |
--constell-overlay-border |
rgba(190,210,255,0.14) |
Info card border colour |
--constell-overlay-blur |
18px |
Info card backdrop blur |
--constell-overlay-shadow |
0 24px 80px rgba(0,0,0,0.32) |
Info card drop shadow |
--constell-eyebrow-color |
rgba(180,220,255,0.78) |
Eyebrow label text colour |
--constell-copy-color |
rgba(230,239,255,0.78) |
Body copy text colour |
Example — purple theming:
:root {
--constell-nebula-a: rgba(180, 60, 255, 0.25);
--constell-overlay-bg: rgba(10, 5, 30, 0.5);
--constell-overlay-border: rgba(200, 160, 255, 0.2);
}Change any setting after init without reloading the page. Partial configs work — only provided keys merge over the current config.
Constell.configure({
star: { count: 80 }, // stars are re-seeded automatically
motion: { frameMs: 16 }, // ~60 fps
visual: { glowRadiusMultiplier: 6 },
});Constell has built-in support for dark and light themes. Both the canvas rendering (stars, nebulae, shooting stars, backdrop gradient) and the CSS overlay elements switch together when you change theme. There are three ways to set a theme:
| Mode | Behaviour |
|---|---|
"dark" |
Always use the dark palette |
"light" |
Always use the light palette |
"system" |
Follow the user's OS preference (prefers-color-scheme) |
The default is "dark". Set it in your init config:
Constell.init("#scene", { theme: "system" });Use Constell.toggleTheme() to change the active theme at runtime. It accepts one of the three mode strings and returns the current mode:
// Set a specific theme
Constell.toggleTheme("light"); // force light
Constell.toggleTheme("dark"); // force dark
Constell.toggleTheme("system"); // follow OS preference
// Toggle: dark ↔ light (system is set at init time, not toggled)
Constell.toggleTheme();Internally, toggleTheme does two things:
- Canvas — applies the theme's colour values (backdrop gradient, star/nebula/shooting-star colors) into the existing config via
Object.assign. Your non-theme settings (motion speed, parallax sensitivity, custom overrides) are preserved. - DOM overlay — sets the
data-theme="dark"ordata-theme="light"attribute on<body>, which triggers CSS variable overrides inconstell.cssfor text and card colors.
Most websites already have a light/dark mode toggle (a button, radio group, select, or framework-level provider). Connect Constell to it by calling Constell.toggleTheme() from your existing handler.
let isLight = false;
document.getElementById("my-theme-toggle").addEventListener("click", () => {
isLight = !isLight;
Constell.toggleTheme(isLight ? "light" : "dark");
});function applyStoredTheme() {
const saved = localStorage.getItem("theme") || "system";
Constell.toggleTheme(saved);
}
// On your site's theme button click:
document.getElementById("site-theme-btn").addEventListener("click", () => {
const current = localStorage.getItem("theme") || "dark";
const next = current === "dark" ? "light" : "dark";
localStorage.setItem("theme", next);
Constell.toggleTheme(next);
});
// On page load:
applyStoredTheme();import { useEffect, useContext } from "react";
import { Constell } from "./constell.js";
import { ThemeContext } from "./theme-context";
function App() {
const { theme } = useContext(ThemeContext); // your app's theme state
useEffect(() => {
if (!document.querySelector("#scene") && Constell.init) {
// Init only once on mount
Constell.init("#scene", { theme });
} else {
// Theme changed after init — sync with our context value
Constell.toggleTheme(theme);
}
}, [theme]); // runs whenever React's theme state changes
return (
<>
<canvas id="scene" />
{/* your site content */}
</>
);
}<script setup>
import { onMounted, watch } from "vue";
import { Constell } from "./constell.js";
import { useTheme } from "./use-theme"; // your theme composable
const { theme } = useTheme();
onMounted(() => {
Constell.init("#scene", { theme: theme.value });
});
// Watch for theme changes after init
watch(theme, (newTheme) => {
Constell.toggleTheme(newTheme);
});
</script>
<template>
<canvas id="scene" />
<!-- your site content -->
</template>---
import { onMount } from "astro:transitions";
const theme = Astro.cookies.has("theme") ? Astro.cookies.get("theme").value : "system";
---
<canvas id="scene"></canvas>
<script is:inline>
import { Constell } from "./constell.js";
Constell.init("#scene", { theme: "{theme}" });
</script>When theme: "system", Constell listens to prefers-color-scheme changes in real time. If the user switches their OS between light and dark modes (e.g. on mobile at sunset, or via a system shortcut), Constell's canvas colours and CSS variables update automatically — no page reload needed.
If you need to react in your own code when that happens:
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
if (Constell.config?.theme === "system") {
// Your app might also want to update fonts, images, etc.
console.log("OS theme changed:", e.matches ? "dark" : "light");
}
});Constell ships two complementary styling systems:
| Layer | Controlled by |
|---|---|
| Canvas content (stars, etc.) | JS config theme + built-in THEMES object |
| DOM overlay (cards, text) | CSS variables via data-theme="light" |
If your site already uses CSS custom properties for theming, you can let Constell's CSS variables follow your existing pattern. Just set the same data-theme attribute on <body> that Constell uses — they are compatible:
/* Your global theme toggle sets this */
[data-theme="light"] {
--constell-bg-start: #e8ecf4;
--constell-bg-end: #f6f8fc;
}The CSS variable values documented in the table above are only applied to the DOM overlay elements (cards, text, gradients behind the canvas). The canvas itself is entirely controlled by the JS config — this separation lets you style your page chrome independently from the animated background.
python -m http.server 7070 --directory .
# open http://localhost:7070