Starfield

A warp-speed pixel starfield. Speed, density and colour are attributes.

zweiundeins <sb-starfield> canvas animation background space Since 2026-09-21 MIT licence 1.5 kB1 Open in playground Edit on GitHub

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 (default 12rem) sets the height.
  • Colours: the sky is --sb-surface-inset. tint picks the stars' token, read whenever it paints (a still field repaints on sb-theme-change and when the system switches between light and dark): white is --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.js one 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:

  • <sb-starfield>
<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.

FileOriginalgzipbrotli
starfield.bundle.min.js 3.1 kB1.7 kB1.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.

FileOriginalgzipbrotliminified
starfield.js 6.1 kB2.6 kB2.3 kB1.4 kB

API reference

Props

AttributeTypeDefaultDescription
speednumber20Flight speed (0 pauses the field).
densitynumber300Number of stars.
tint"white" | "violet" | "cyan" | "green""white"Star colour (from theme tokens).
warpbooleanfalseDraw streaks instead of dots.
labelstring"Animated starfield"Accessible label (empty for a decorative background).