Autoloader

Loads any web component the first time its tag appears, morphs included.

zweiundeins <sb-autoloader> loader lazy modules custom-elements infrastructure Since 2026-09-23 1.7 kB0Open in playground Edit on GitHub

Playground

loaded on demand

Put it on a page and every custom element loads itself the first time its tag shows up: on the first render, and later too, when a Datastar morph or a script adds markup. Nothing else changes, and pages ship only the components they actually use.

It is the generic version of the autoloader this site serves at /c/autoloader.js, which carries Starbase's own tag map. This one takes the map (or a URL pattern) from you, and works with any web component, not only Rocket ones.

Examples

A map of tags

loaded on demand
<sb-autoloader modules='{"demo-badge": "/c/autoloader/demo-badge.js"}'></sb-autoloader>
<demo-badge>loaded on demand</demo-badge>

demo-badge is a plain custom element with no dependencies. Nothing on the page imports it; the autoloader fetches it because the tag is there.

A pattern for a whole folder

With a naming convention, one line covers every component. {tag} is the element's name, and match keeps it to your own tags, so unrelated elements (a third-party widget, an icon element) are left alone:

<sb-autoloader pattern="/components/{tag}/{tag}.js" match="^x-"></sb-autoloader>

<x-chart></x-chart>   <!-- loads /components/x-chart/x-chart.js -->
<x-table></x-table>   <!-- loads /components/x-table/x-table.js -->

Entries in modules win over the pattern, so exceptions stay easy:

<sb-autoloader
  pattern="/components/{tag}/{tag}.js"
  match="^x-"
  modules='{"x-legacy": "https://cdn.example.com/legacy/bundle.js"}'
></sb-autoloader>

No flash of undefined elements

Custom elements are empty until their module arrives. Put a class on <html>, hide undefined elements with it, and name it in cloak: it is removed once the first round of components is defined, or after timeout (3 s by default), so a failed module can never leave the page blank.

<html class="loading">
<style>.loading :not(:defined) { visibility: hidden }</style>

<sb-autoloader pattern="/c/{tag}/{tag}.js" cloak="loading"></sb-autoloader>

Watch what it loads

sb-load fires per component. sb-ready fires once, when the components that were on the page at startup are defined; later arrivals keep firing sb-load only.

a tag of its own

<div data-signals="{_log: 'waiting…'}">
  <sb-autoloader
    modules='{"demo-card": "/c/autoloader/demo-badge.js?tag=demo-card"}'
    data-on:sb-load="$_log = 'loaded <' + evt.detail.tag + '>'"
    data-on:sb-ready="$_log += ' · ready (' + evt.detail.loaded + ')'"
    data-on:sb-load-error="$_log = 'could not load <' + evt.detail.tag + '>: ' + evt.detail.error"
  ></sb-autoloader>
  <demo-card>a tag of its own</demo-card>
  <p data-text="$_log"></p>
</div>

(The demo module takes the tag it defines from ?tag=, so this example loads something the examples above haven't.) A tag that is already defined is never fetched again, so an autoloader that finds nothing to do goes straight to sb-ready with loaded: 0.

From JavaScript, el.ready is a promise that resolves with the number of components loaded, and el.load('x-chart') fetches one by hand.

Dependencies between components

A component that renders another tag inside its own shadow root would otherwise only be discovered after it rendered. List those tags in requires and they load together:

<sb-autoloader
  pattern="/c/{tag}/{tag}.js"
  requires='{"sb-code-playground": ["sb-code-editor"]}'
></sb-autoloader>

Notes

  • Wrapping is optional. <sb-autoloader> works as a single tag anywhere on the page; children render untouched, so you can also wrap the markup that needs the components.
  • Once per tag. Two autoloaders on a page, or one that survives a morph, never fetch the same module twice. A failed load is retried when the tag appears again.
  • Shadow roots. It watches the document, so components inside other components' shadow roots are discovered when those render into the page, not before. requires covers the rest.
  • Rocket components import datastar, so the page still needs the import map for it. Plain custom elements need nothing.
  • A failed module reports through reportError (so window.onerror sees it) and emits sb-load-error. A match that isn't a valid regular expression is reported the same way, and the pattern is skipped.
  • demo-badge.js in this component's folder belongs to the examples above, not to the loader; that is why it shows up in the size table. It is served at /c/autoloader/demo-badge.js, so in a pull-request preview (where the component is not in the catalog yet) the examples report a failed load: the loader works, the demo module is simply not there.

Accessibility

The element has no box of its own (display: contents) and no role; it adds nothing to the accessibility tree and takes no focus. Children, if you wrap any, render exactly where they are written. Use cloak so people don't see half-built elements, and remember the page must work if a module never arrives.

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 added later by a Datastar morph.

<!-- Once per page: Datastar with Rocket, and the Starbase autoloader.
     It loads every <sb-…> component the first time its tag appears. -->
<script type="importmap">
  { "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/autoloader.js"></script>
<!-- Optional, no flash of undefined elements: class="sb-cloak" on <html>, and -->
<style>.sb-cloak :not(:defined) { visibility: hidden }</style>

<sb-autoloader modules='{"demo-badge": "/c/autoloader/demo-badge.js"}'></sb-autoloader>
<demo-badge>loaded on demand</demo-badge>

<!-- In production, pin today's catalog instead of the latest: the browser then
     refuses any file that changed. Add "integrity" to the import map above: the
     hashes from https://starbase.zweiundeins.gmbh/c/@722e189276ac/importmap.json and Datastar's, below. -->
<!--
<script type="importmap">
  { "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" },
    "integrity": { "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js": "sha384-vUxZojLrF1Ar3de5h7VINqhJXBgjyZS4U49pHvGa87kum6j5Xn6JRziuARNw5ELG", "…": "…from importmap.json" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/@722e189276ac/autoloader.js" integrity="sha384-scFa7/4jztDOx1YZT9/1g1ULYm9LsDlQn/8hMTyyD4IkPnZV2pfhHytoNTWTJG6b"></script>
-->

<!-- Or load just this component, pinned to this version. The minified module
     is what the autoloader uses; the readable source is the same URL without .min. -->
<!-- <script type="module" src="https://starbase.zweiundeins.gmbh/c/autoloader@7d520cd36d58/autoloader.min.js" integrity="sha384-hwGY0BgfepzNNW5x0U3WyxujY0HawM17eb4yFuyW423pcjEE6CG9Av5IPdyFY02R"></script> -->

Size

Each file compressed on its own, the way it is served (gzip -9, brotli -11). The autoloader loads the minified files (esbuild), so that last column is what a page downloads; the readable source is always there too. Datastar and Rocket are shared by every component and not counted.

FileOriginalgzipbrotliminified
autoloader.js 5.6 kB2.3 kB1.9 kB1.3 kB
demo-badge.js 998 B643 B537 B373 B
Total6.6 kB2.9 kB2.5 kB1.7 kB

API reference

Props

AttributeTypeDefaultDescription
modulesjson{}Explicit map of tag to module URL: {"x-chart": "/js/x-chart.js"}. Wins over pattern.
patternstring""URL template for tags not in modules, with {tag}, e.g. "/components/{tag}/{tag}.js".
matchstring""Regular expression the tag must match before pattern applies, e.g. "^x-". Empty: every unknown custom element.
requiresjson{}Tags a component renders itself, loaded with it: {"x-table": ["x-cell"]}.
basestring""Base for relative URLs. Default: the document's base URL.
cloakstring""Class removed from <html> once the first components are defined (against the flash of undefined elements).
timeoutnumber3000Remove the cloak class after this many ms, whatever happens.

Events

NameDescription
sb-loadAfter a component is defined. detail: { tag, url }.
sb-load-errorWhen a module fails to load. detail: { tag, url, error }.
sb-readyOnce, when the components present at startup are defined (right away when there were none). detail: { loaded }.