Playground
Stars fly toward you in chunky pixels. Every knob is an attribute, so a slider (or the server) can throttle up to warp. Colours come from theme tokens, so the field follows the page's theme.
Examples
Throttle
<div data-signals="{_speed: 20, _warp: false}" style="display: grid; gap: 16px; inline-size: 100%">
<sb-starfield data-attr:speed="$_speed" data-attr:warp="$_warp" data-preserve-attr="speed warp" tint="cyan"></sb-starfield>
<div style="display: flex; gap: 24px; align-items: end; flex-wrap: wrap">
<sb-slider label="Speed" max="100" style="flex: 1; min-inline-size: 12rem" data-bind:_speed__prop.value></sb-slider>
<sb-toggle label="Warp" data-bind:_warp__prop.checked></sb-toggle>
</div>
</div>
Tints
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); gap: 12px; inline-size: 100%">
<sb-starfield tint="white" style="--sb-starfield-height: 6rem"></sb-starfield>
<sb-starfield tint="violet" style="--sb-starfield-height: 6rem"></sb-starfield>
<sb-starfield tint="cyan" style="--sb-starfield-height: 6rem"></sb-starfield>
<sb-starfield tint="green" warp speed="60" style="--sb-starfield-height: 6rem"></sb-starfield>
</div>
Performance
It renders at a third of the CSS resolution into one ImageData. It stops animating offscreen, when speed is 0, or under prefers-reduced-motion, where it shows a single still frame with every star. Set the height with --sb-starfield-height.
Styling
Style it from your page's CSS, without changing the component or importing anything into it. Custom properties, inherited properties and ::part() all reach into its shadow root.
- Size: it fills the width it is given;
--sb-starfield-height(default12rem) sets the height. - Colours: the sky is
--sb-surface-inset.tintpicks the stars' token, read whenever it paints (a still field repaints onsb-theme-changeand when the system switches between light and dark):whiteis--sb-text-1,violet--sb-brand-light,cyan--sb-accent,green--sb-datastar. Corners are--sb-radius. - Parts:
canvas. Your page's::part()rules win over the component's own, without!important.
<style>
.my-stars { --sb-starfield-height: 6rem; --sb-radius: 0; --sb-accent: #F0ABFC; }
</style>
<sb-starfield class="my-stars" tint="cyan"></sb-starfield>
Accessibility
The canvas is an image named by label (default "Animated starfield"): translate it there, not with aria-label on the element. Where the field is a decorative background, set label="" and assistive technology skips it (so does aria-hidden="true" on the element). Don't rely on it to convey information.
The motion stops under prefers-reduced-motion. It runs until you stop it, so give everyone else a way to pause it (WCAG 2.2.2): speed="0" holds it on a still frame.
Installation
Add Datastar with Rocket and the Starbase autoloader once per page, then use the tag: the autoloader imports each component the first time its tag appears, including tags a Datastar morph adds later. The Datastar here is v1.0.4 with Starbase's fixes until they are released; the components also run on the official release.
<script type="importmap">
{ "imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/autoloader.js"></script>
<sb-starfield></sb-starfield>No flash of undefined elements: put class="sb-cloak" on <html> and add .sb-cloak :not(:defined) { visibility: hidden } to your CSS. The autoloader removes the class once the page's components are defined.
Load just this component, pinned to this version: all its modules in one minified file with its integrity hash, so the browser refuses it if a single byte changes.
<script type="importmap">
{
"imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
"integrity": {
"https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5"
}
}
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/starfield@3cc4f2aefad6/starfield.bundle.min.js" integrity="sha384-ZJnUOQ1b8BoYpzJaFN82tSasBfew6BtHZ/aGi5S+JbWeZpEZMIzC/qEU6Os7tagI"></script>
<sb-starfield></sb-starfield>Or the minified module files (one file, which the browser requests as it finds their imports), each pinned in the import map:
<script type="importmap">
{
"imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
"integrity": {
"https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5",
"https://starbase.zweiundeins.gmbh/c/starfield@3cc4f2aefad6/starfield.min.js": "sha384-O4ogB0dJBQ9DwFva9f0LEWuW2e0DSe7hD8ZcFVspC1X4UKCbuh62+5fPBy/+QuzD"
}
}
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/starfield@3cc4f2aefad6/starfield.min.js" integrity="sha384-O4ogB0dJBQ9DwFva9f0LEWuW2e0DSe7hD8ZcFVspC1X4UKCbuh62+5fPBy/+QuzD"></script>
<sb-starfield></sb-starfield>In production, pin today's whole catalog: its autoloader and an import map with integrity hashes, so the browser refuses any file that changed. This map covers Datastar and this component; importmap.json has the hashes of every component, to merge into yours.
<script type="importmap">
{
"imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
"integrity": {
"https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5",
"https://starbase.zweiundeins.gmbh/c/starfield@3cc4f2aefad6/starfield.bundle.min.js": "sha384-ZJnUOQ1b8BoYpzJaFN82tSasBfew6BtHZ/aGi5S+JbWeZpEZMIzC/qEU6Os7tagI"
}
}
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/@339249aca48f/autoloader.js" integrity="sha384-ZNpMayI1XShztIEX0iDHyCDDbhT8H+t+zEESUq4BCrN4b+7m931LQEl5x0vbY+Vo"></script>
<sb-starfield></sb-starfield>Components import only datastar and their own files, so copying is enough: save each component's one file (and a file it loads on first use, keeping its folder), and point the import map at your own copy of datastar-rocket.js (the /js/ paths stand for yours).
<sb-starfield>starfield.bundle.min.jsone file 1.5 kB brotli
<script type="importmap">
{ "imports": { "datastar": "/js/datastar-rocket.js" } }
</script>
<script type="module" src="/js/starfield/starfield.bundle.min.js"></script>
<sb-starfield></sb-starfield>Or copy the module files, keeping each component's folder, and load each component's module:
<script type="importmap">
{ "imports": { "datastar": "/js/datastar-rocket.js" } }
</script>
<script type="module" src="/js/starfield/starfield.min.js"></script>
<sb-starfield></sb-starfield>Size
A page loads this component as one file, its bundle, compressed the way it is served (gzip -9, brotli -11): brotli is what it downloads. Datastar and Rocket are shared by every component and not counted.
| File | Original | gzip | brotli |
|---|---|---|---|
starfield.bundle.min.js | 3.1 kB | 1.7 kB | 1.5 kB |
As module files: one request, 1.4 kB minified with brotli
For a page that loads the module files as they are, requesting each one as it finds its import, or bundles them itself. Each file compressed on its own; the minified one is what the page downloads, and the readable source is always there too.
| File | Original | gzip | brotli | minified |
|---|---|---|---|---|
starfield.js | 6.1 kB | 2.6 kB | 2.3 kB | 1.4 kB |
API reference
Props
| Attribute | Type | Default | Description |
|---|---|---|---|
speed | number | 20 | Flight speed (0 pauses the field). |
density | number | 300 | Number of stars. |
tint | "white" | "violet" | "cyan" | "green" | "white" | Star colour (from theme tokens). |
warp | boolean | false | Draw streaks instead of dots. |
label | string | "Animated starfield" | Accessible label (empty for a decorative background). |