Skip to content

Getting started

  • Alpine.js v3+
  • A bundler with ESM support (Vite, Webpack, etc.) or native ES modules

Start with the core primitives and the five essential modules:

Terminal window
pnpm add alpinejs \
@ailuracode/alpine-core \
@ailuracode/alpine-ui \
@ailuracode/alpine-theme \
@ailuracode/alpine-toggle \
@ailuracode/alpine-media \
@ailuracode/alpine-scroll \
@ailuracode/alpine-sidebar \
@ailuracode/alpine-toast

Add more packages later — each one is an independent npm dependency.

Section titled “Direct registration (recommended for most apps)”

Every feature plugin is a plain Alpine callback. Register them in your app entry, then start Alpine:

import Alpine from "alpinejs";
import { themePlugin } from "@ailuracode/alpine-theme";
import { mediaPlugin } from "@ailuracode/alpine-media";
import { scrollPlugin } from "@ailuracode/alpine-scroll";
Alpine.plugin(themePlugin());
Alpine.plugin(mediaPlugin());
Alpine.plugin(scrollPlugin());
Alpine.start();

@ailuracode/alpine-core ships the registration guards so a second plugin that asks for $store.theme throws RegistrationError instead of silently overwriting the host’s store.

If your bundler splits dynamic imports into separate chunks, register heavy plugins through a thin wrapper that resolves the chunk lazily:

import Alpine from "alpinejs";
const plugins = [
() => import("@ailuracode/alpine-theme").then((m) => m.themePlugin()),
() => import("@ailuracode/alpine-media").then((m) => m.mediaPlugin()),
() => import("@ailuracode/alpine-toast").then((m) => m.toastPlugin()),
];
await Promise.all(plugins.map((load) => load().then((p) => Alpine.plugin(p))));
Alpine.start();

This gives you code-splitting without a dedicated registry abstraction — the feature plugins themselves stay Alpine callbacks, which keeps them discoverable and easy to test.

<button @click="$store.theme.set('dark')">Dark</button>
<button @click="$store.theme.set('light')">Light</button>
<button @click="$store.theme.set('system')">System</button>
<button @click="$store.theme.toggle()">Toggle</button>
<div x-show="$store.media.matches('mobile')">Mobile layout</div>
<button x-show="$store.scroll.showToTop" @click="$store.scroll.toTop()">
Back to top
</button>
<button @click="$toast('Changes saved', { variant: 'success' })">Notify</button>

Push a plain payload from server-rendered data or events:

<div
x-data
x-init="$toast.fromPayload({ title: 'Saved', variant: 'success' })"
></div>
Tier Packages When to add
Essentials theme, media, scroll, sidebar Most Alpine apps
Headless UI dialog, menu, tooltip, toast, tabs, accordion, command, carousel Accessible UI you style yourself
Extended network, attention, clipboard, platform, toggle Connectivity, clipboard, device hints
Advanced geo, battery, export, share, attention, notify, calendar, json-api Specialized browser APIs
Query query + adapter + devtools Client-side data cache (see Query)
<script type="module">
import Alpine from "https://esm.sh/alpinejs";
import { themePlugin } from "https://esm.sh/@ailuracode/alpine-theme";
Alpine.plugin(themePlugin());
Alpine.start();
</script>

To react to theme transitions from a CDN snippet:

<script type="module">
import Alpine from "https://esm.sh/alpinejs";
import { themePlugin } from "https://esm.sh/@ailuracode/alpine-theme";
Alpine.plugin(themePlugin());
Alpine.start();
// Apply classes via the $theme magic once Alpine is ready
document.addEventListener("alpine:init", () => {
Alpine.store("theme").on("change", (detail) => {
document.documentElement.classList.toggle("dark", detail.resolved === "dark");
});
});
</script>

Each package ships dist/index.d.ts (imports) and dist/global.d.ts (Alpine augmentations):

/// <reference types="@types/alpinejs" />
/// <reference types="@ailuracode/alpine-core" />
/// <reference types="@ailuracode/alpine-theme" />
/// <reference types="@ailuracode/alpine-toast" />

Or import the plugin module — generated types augment globals automatically.

Alpine silently overwrites whatever a previous plugin registered under the same key. Toolkit feature plugins guard their registrations through @ailuracode/alpine-core and throw RegistrationError("REGISTRATION_COLLISION") instead of clobbering the host’s store or magic.

If the host already owns a name (own $store.theme, sibling toolkit plugin registered $store.toast, etc.) the recommended fix is renaming the integration surface, not overriding:

Alpine.plugin(themePlugin({ storeKey: "appearance" })); // → $store.appearance
Alpine.plugin(toastPlugin({ magicKey: "notify" })); // → $notify

Feature plugin options accept storeKey / magicKey (and the matching one for directives) for exactly this case. The escape hatch is the { override: true } option on guardStore / guardMagic / guardDirective, but prefer renaming — silent overwrites are the bug class this guard exists to catch.

See Core — Avoiding name collisions for the full API and the architecture:check rule that enforces it.