AlpineComponentLoader (ACL) loads reusable HTML components with Alpine.js from URLs, DOM templates, inline strings, or
template[x-acl] declarations. Optional capabilities add data fetching, persistence, hydration, forms, offline support,
and development tools.
These docs cover package 1.x. ACL document schemas use version 1; external standards retain their own versions. See
format and schema versions.
Use jsDelivr with an import map for browser projects that need no installation or application build.
Add this to the document <head>:
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.17.4/dist/cdn.min.js"></script>
<script type="importmap">
{
"imports": {
"alpine-component-loader": "https://cdn.jsdelivr.net/gh/LouisT/alpine-component-loader@latest/dist/index.min.js",
"alpine-component-loader/core": "https://cdn.jsdelivr.net/gh/LouisT/alpine-component-loader@latest/dist/core.min.js",
"alpine-component-loader/auto": "https://cdn.jsdelivr.net/gh/LouisT/alpine-component-loader@latest/dist/auto.min.js",
"alpine-component-loader/dev": "https://cdn.jsdelivr.net/gh/LouisT/alpine-component-loader@latest/dist/dev.min.js",
"alpine-component-loader/debugger": "https://cdn.jsdelivr.net/gh/LouisT/alpine-component-loader@latest/dist/debugger.min.js",
"alpine-component-loader/a11y": "https://cdn.jsdelivr.net/gh/LouisT/alpine-component-loader@latest/dist/a11y.min.js",
"alpine-component-loader/a11y-scanner": "https://cdn.jsdelivr.net/gh/LouisT/alpine-component-loader@latest/dist/a11y-scanner.min.js",
"alpine-component-loader/offline": "https://cdn.jsdelivr.net/gh/LouisT/alpine-component-loader@latest/dist/offline.min.js",
"alpine-component-loader/observability-exporters": "https://cdn.jsdelivr.net/gh/LouisT/alpine-component-loader@latest/dist/observability-exporters.min.js",
"alpine-component-loader/polyfills": "https://cdn.jsdelivr.net/gh/LouisT/alpine-component-loader@latest/dist/polyfills.min.js",
"alpine-component-loader/testing": "https://cdn.jsdelivr.net/gh/LouisT/alpine-component-loader@latest/dist/testing.min.js"
}
}
</script>For production, replace RELEASE_TAG with a release tag or commit containing the entries you use. Older releases may
lack /core or /polyfills:
<script type="importmap">
{
"imports": {
"alpine-component-loader": "https://cdn.jsdelivr.net/gh/LouisT/alpine-component-loader@RELEASE_TAG/dist/index.min.js",
"alpine-component-loader/core": "https://cdn.jsdelivr.net/gh/LouisT/alpine-component-loader@RELEASE_TAG/dist/core.min.js",
"alpine-component-loader/polyfills": "https://cdn.jsdelivr.net/gh/LouisT/alpine-component-loader@RELEASE_TAG/dist/polyfills.min.js"
}
}
</script>Import-map entries download only when imported.
Import ACL from your application module:
<script type="module" src="/js/components.js"></script>// /js/components.js
import AlpineComponentLoader from 'alpine-component-loader';
AlpineComponentLoader.define('profile-card', '/components/profile-card.html');
await AlpineComponentLoader.start();npm run build emits readable .js and prebuilt .min.js files for every dist/ module. Publish the complete
directory to GitHub or a static host. Entries request dependencies in their own readable/minified family; keep that
family consistent.
The entry loads optional implementations only when markup, configuration, or an API call activates them.
alpine-component-loader/core exports the default loader and ACLLoadError. Its start() registers queued definitions
without document discovery:
import Loader from 'alpine-component-loader/core';
await Loader.define('profile-card', { template: '<p>Profile</p>' });
await Loader.start();For CDN delivery, map /core to dist/core.js or dist/core.min.js. Root and core share the default loader; a later
root start() enables discovery. Both offer explicit observation. Isolated-loader and persistence-adapter factories are
root-entry exports.
The optional /polyfills entry loads needed Web Components fallbacks and waits for registration
readiness. Native browsers skip upstream downloads; HTML Imports are excluded.
Choose root for discovery, /core for explicit registration, or /auto for import-time startup. Core applications must
register built-ins such as <acl-boundary> explicitly. See core startup.
Load polyfills in a separate bootstrap before dynamically importing the application. Use Alpine's ESM setup from the polyfill guide for this ordering:
// bootstrap.js; the import map includes /polyfills and /core.
import loadPolyfills from 'alpine-component-loader/polyfills';
await loadPolyfills();
await import('./app.js');app.js starts Alpine once, registers ACL definitions, and awaits start(). The polyfill guide
covers CDN/Vite setup, WebComponents.waitFor(), CSP, and offline assets. ACL still requires native ESM/ES2022.
Inline eager components skip unused networking, caches, data, hydration, transformations, environment, and scheduling modules. Loaders share implementation URLs while keeping runtime state isolated.
The development server serves physical .min.js files first and generates missing siblings from .js in memory. Source
changes invalidate generated responses. HMR mappings follow the application's base URL and suffix. See
server setup.
Install the GitHub package for CLI tools, Node SSR, or package-manager integration:
npm install github:LouisT/alpine-component-loader alpinejsDefault installs include optional parse5 and @swc/core tooling. To omit them:
npm install --omit=optional github:LouisT/alpine-component-loader alpinejsLean installs retain the runtime, basic CLI commands, and prebuilt JavaScript serving. Features needing an omitted tool
report ACL_OPTIONAL_DEPENDENCY_MISSING with an installation command.
Install parse5 for inspection, manifest generation, inline-template HMR, or SSR; install @swc/core for on-demand
minification and distribution builds.
The installed CLI runs through npx:
npx alpine-component-loader --help
npx alpine-component-loader serve
npx alpine-component-loader create my-app
npx alpine-component-loader audit / --root my-app
npx alpine-component-loader routes --config acl.config.mjs
npx alpine-component-loader watch --config acl.config.mjsFor local development:
npm install --save-dev file:../alpine-component-loader
npx alpine-component-loader --helpTo run a local checkout without installing it:
npx --package=. -- alpine-component-loader --helpFor a one-off GitHub invocation:
npx --package=github:LouisT/alpine-component-loader -- alpine-component-loader --helpPin GitHub installs to a tag or commit. Install the package before using bare npx alpine-component-loader, which
otherwise falls back to the npm registry.
Keep ACL external to application bundles and serve dist/ unchanged. Runtime capability imports resolve relative to the
entry URL.
ACL supports Alpine.js 3.14-3.x and modern Chrome, Edge, Firefox, and Safari. Node tooling requires Node.js 22+;
nvm use selects the repository minimum.
Create a starter:
npx alpine-component-loader create my-app
npx alpine-component-loader create my-vite-app --template viteAfter adding the import map, register components:
import AlpineComponentLoader from 'alpine-component-loader';
AlpineComponentLoader.config({ basePath: '/components/' });
// start() drains this pending pre-start definition.
AlpineComponentLoader.define('profile-card', 'profile-card.html', {
shadow: true,
attributes: {
name: { type: String, required: true },
count: { type: Number, default: 0 },
},
});
await AlpineComponentLoader.start();define() returns a promise. start() waits for pre-start definitions; await define() when you need its constructor
immediately.
<profile-card name="Ada" count="3">
<button slot="action">Follow</button>
</profile-card>Inline definitions use the same Trusted Types and sanitizer pipeline as URL templates, without fetching or persistent template caching:
await AlpineComponentLoader.define('inline-greeting', {
template: '<h3>Hello, world!</h3>',
shadow: true,
});An existing DOM template also works without a request:
<template id="profile-template">
<article x-data="{ open: false }">
<h2 x-text="$props.name"></h2>
<slot name="action"></slot>
</article>
</template>await AlpineComponentLoader.define('profile-card', '#profile-template', { shadow: true });x-acl names a declarative component. Put acl-props and data options on its definition; instances may override data
attributes:
<template x-acl="profile-template" acl-props='{ "name": "String" }' data-src="/api/profile">
<article x-data="{ open: false }">
<h2 x-text="$props.$data?.name"></h2>
<slot name="action"></slot>
</article>
</template>
<profile-template>
<button slot="action">Follow</button>
</profile-template>start() scans existing template[x-acl] and template[acl-component] declarations. observeTemplates() handles
later additions. /auto enables observation by default; configure it before import to opt out:
<script>
window.AlpineComponentLoaderConfig = {
observeTemplates: false,
};
</script>Custom elements need closing tags: <profile-template></profile-template>, not XML self-closing syntax.
The declarative proxy provides the same registration path from HTML:
<acl-component
src="/components/profile-card.html"
tag="profile-card"
shadow="true"
loading="lazy"
acl-props='{ "name": { "type": "String", "required": true } }'
name="Ada"
>
<button slot="action">Follow</button>
</acl-component>start() discovers <acl-component>, <acl-dynamic>, and <acl-boundary> as they appear. For registration before
insertion, await registerComponent(), registerDynamicLoader(), or registerErrorBoundary().
/auto starts on import. Other entries are safe to import without browser globals; browser operations require a DOM.
acl.config.mjs shares paths and policies between CLI and Vite. Paths resolve from that file; unknown keys fail
validation. Precedence is CLI flags → plugin options → project config → defaults:
import { defineConfig } from 'alpine-component-loader/project';
export default defineConfig({
components: {
directory: './public/components',
manifest: './public/acl-manifest.json',
inference: 'safe',
},
routes: {
manifest: './public/acl-manifest.json',
outDir: './public/acl-routes',
entries: [
{ key: 'home', path: '/', components: ['home-page'] },
{ key: 'account', path: '/account', groups: ['account'], discover: true },
],
},
contracts: {
types: './generated/acl-components.d.ts',
customElements: './generated/custom-elements.json',
},
watch: { tasks: ['manifest', 'types', 'routes'], debounce: 100, pollInterval: 5000 },
});Generate artifacts or watch for changes:
npx alpine-component-loader manifest --update
npx alpine-component-loader manifest --infer report --dry-run
npx alpine-component-loader routes --force
npx alpine-component-loader watch
npx alpine-component-loader watch --poll-interval 0Watch mode uses native subscriptions with directory fallbacks and a periodic safety scan (watch.pollInterval or
--poll-interval; 0 disables it). Servers expose watchPollInterval. Safe inference updates certain dependencies,
slots, and static assets; sidecars remain authoritative. Browser-backed discovery, skeletons, and audits are opt-in.
In Vite, enable the plugin and register route keys through its virtual module:
// vite.config.js
import { defineConfig } from 'vite';
import { alpineComponentLoader } from 'alpine-component-loader/vite';
export default defineConfig({
base: '/my-app/',
plugins: [alpineComponentLoader()],
});import AlpineComponentLoader from 'alpine-component-loader';
import { registerRoute } from 'virtual:alpine-component-loader/routes';
const start = async () => {
await registerRoute('account');
await AlpineComponentLoader.start();
};
start();Vite serves native modules and targeted HMR in development. Production copies ACL modules and route shards, then injects a base-aware import map. See the Vite starter.
- Components: templates, props, slots, styles, Shadow DOM, assets, and skeletons.
- Data: fetching, polling, caches, and persistence.
- Lifecycle: hydration, events, forms, boundaries, reloads, and cleanup.
- Project tooling: manifests, inference, routes, Vite, and generated contracts.
- SSR, prefetch, and offline support.
- Testing, observability, and accessibility/debugging.
- Security, core startup, polyfills, and the API reference.
Fetch component data and emit a composed application event:
await AlpineComponentLoader.define('account-card', '/components/account-card.html', {
attributes: { accountId: { type: String, required: true } },
data: {
src: '/api/accounts/:accountId',
keys: ({ props }) => ({ accountId: props.accountId }),
target: 'account',
cacheStrategy: 'network-first',
retries: 2,
},
});<article>
<p x-show="$props.$loading" role="status">Loading…</p>
<h2 x-text="$props.account?.name"></h2>
<button @click="$props.$emit('account-select', { id: $props.accountId })">Select</button>
</article>Register a manifest:
const manifest = await fetch('/acl-manifest.json').then((response) => response.json());
await AlpineComponentLoader.registerManifest(manifest, {
prefetch: ['critical'],
concurrency: 2,
});
await AlpineComponentLoader.start();Or use the bounded URL and exact route-shard loaders:
await AlpineComponentLoader.registerManifestFrom('/acl-manifest.json', {
cache: 'no-cache',
signal: abortController.signal,
integrity: 'sha256-…',
});
await AlpineComponentLoader.registerRouteManifest('account', '/routes/acl-routes.json');URL/integrity requests share work per loader and retry after failure. Relative URLs resolve from the fetched manifest or route index.
Defer valid SSR hydration until interaction, and contain descendant failures at the nearest boundary:
await AlpineComponentLoader.define('account-card', '/components/account-card.html', {
shadow: true,
templateRevision: 'sha256-…',
hydrate: 'interaction',
});<acl-boundary>
<account-card></account-card>
<p slot="fallback" role="alert">Account details are temporarily unavailable.</p>
</acl-boundary>SSR hydration also supports visible, idle, and media with hydrateMedia. Client rendering uses loading.
Opt a definition into the native form lifecycle with JSON-safe prop mappings:
await AlpineComponentLoader.define('quantity-field', '/components/quantity-field.html', {
attributes: {
quantity: { type: Number, default: 1 },
restoredState: String,
disabledState: Boolean,
},
form: {
value: 'quantity',
state: 'restoredState',
disabled: 'disabledState',
},
});Hosts expose validity methods and $props.$form, with hidden-input submission when ElementInternals is unavailable.
Observe opt-in prefetch targets:
const prefetchController = await AlpineComponentLoader.observePrefetch();
// <a data-acl-prefetch="critical">Account</a>Read local metrics:
AlpineComponentLoader.config({ observability: { bufferSize: 200 } });
const unsubscribe = AlpineComponentLoader.subscribe((record) => console.debug(record));
const snapshot = AlpineComponentLoader.getMetrics();
unsubscribe();Batch and deliver the same redacted records through a demand-loaded entry:
import { connectExporter, createBeaconExporter } from 'alpine-component-loader/observability-exporters';
const exporterConnection = connectExporter(
AlpineComponentLoader,
createBeaconExporter({
url: '/telemetry/acl',
batchSize: 20,
flushInterval: 5000,
maxQueue: 200,
}),
);OpenTelemetry and Sentry adapters accept caller-owned clients and do not add vendor SDKs to ACL.
Mount a component in a browser test:
import { createACLTestHarness } from 'alpine-component-loader/testing';
const harness = createACLTestHarness();
const mounted = await harness.mount({ template: '<p>Ready</p>' });
await harness.cleanup();Render with server side Node:
import { createSSRRenderer } from 'alpine-component-loader/ssr';
const renderer = createSSRRenderer({
manifest,
root: process.cwd(),
dataResolver: async ({ tagName, props }) => (tagName === 'profile-card' ? loadProfile(props.name) : undefined),
});
const html = await renderer.render('profile-card', {
props: { name: 'Ada', count: 3 },
slots: { action: '<button>Follow</button>' },
hydrate: 'visible',
});dataPolicy follows JSON-safe component requests instead of using dataResolver. It requires baseUrl and origin
authorization, validates redirects, defaults to GET/HEAD, strips sensitive headers, and limits time and bytes:
const policyRenderer = createSSRRenderer({
manifest,
root: process.cwd(),
dataPolicy: {
baseUrl: 'https://api.example.com/',
allowedOrigins: ['https://api.example.com'],
resolve: ({ props }) => ({ keys: { id: props.id } }),
},
});Create an accessibility baseline after review, then enforce it with expiring suppressions:
npx alpine-component-loader audit / --root public \
--baseline .acl/a11y-baseline.json --update-baseline
npx alpine-component-loader audit / --root public \
--baseline .acl/a11y-baseline.json \
--suppressions .acl/a11y-suppressions.json \
--format sarif --out reports/a11y.sarifSee the example catalog for runnable demonstrations.
- Components, props, slots, and rendering
- Data, caching, polling, and persistence
- Lifecycle, events, and dynamic components
- Security and Trusted Types
- SSR and hydration
- Testing utilities
- Skeleton loading UI
- Observability
- Adaptive prefetch
- Manifests, generated contracts, and CLI tooling
- Offline behavior
- Accessibility and debugging
- API reference and troubleshooting
- Migration notes
- Security disclosure policy
Repository builds use the exact devDependencies.alpinejs-pinned pin in package.json. Update it, run npm install,
then npm run build to refresh CDN URLs, examples, starters, and offline workers. Staging, capture, and benchmarks use
the same alias.
Prettier formats JavaScript, HTML/SSR templates, CSS, and Markdown. Markdown prose wraps at 120 columns. Format everything or one language:
npm run format
npm run format:js
npm run format:html
npm run format:cssnpm run check:format checks all four formats; check:format:js, :html, :css, and :md check them separately. CI
and validate enforce these rules. .prettierignore excludes generated files, dependencies, and staging output.
When formatting changes a template with a pinned templateRevision, refresh its manifest with the CLI's
manifest --update command.
npm ci
npm run playwright:install -- chromium firefox webkit
npm run validate
npm run test:cross-browser
npm run security:static
npm run check:reproducibleFor a locally validated direct push to main, GitHub's [skip actions] commit marker skips push workflows:
git commit -m "chore: update repository metadata [skip actions]"
git push origin mainAvoid this marker on pull-request HEAD commits: skipped required checks may block merging. See GitHub's workflow-skipping reference.
MIT