JavaScript and NPM
Blade-only components add no JavaScript. Interactive components ship one behaviour module each — a
plain ES module that registers an Alpine.js 3.x data component — copied
into resources/js/ui and bundled by your own Vite build. There is no
runtime package, no global script tag, and nothing loaded from a CDN.
The contract, in one table
Every item declares its JavaScript need in its manifest (api.javascript);
ui:show <item> prints it and the docs show it as a badge.
| Declared as | Without JavaScript | Examples |
|---|---|---|
| none | The complete component works. | button, card, badge, table |
| progressive enhancement | Core content and native actions work; motion, positioning, or feedback can be absent. | accordion, tooltip, toast |
| required | Content stays readable and causes no browser error, but the custom interaction is unavailable. | dialog, combobox, command palette |
The full support matrix, browser window, and bundler requirements are on the Compatibility page.
Wiring
Alpine boots from your entrypoint
ui:install writes resources/js/ui/index.js and
adds one import to resources/js/app.js. If Alpine is not installed yet,
ui:install --alpine writes the three bootstrap lines for you.
import Alpine from 'alpinejs';
import './ui/index.js'; // every installed behaviour module, in one place
window.Alpine = Alpine;
Alpine.start();
ui:add appends one import per behaviour module
Items that ship JavaScript declare it in files[] with
type: "js". The CLI copies the file and appends its import to
index.js; ui:remove prunes it again.
Imports must land before Alpine.start(), which is why they are
collected here rather than in your page templates.
import './dialog.js';
import './dropdown.js';
import './ai/prompt.js';
A behaviour module registers an Alpine data component
The Blade file carries x-data="uiDialog({ … })"; the module
registers uiDialog on alpine:init.
Config is passed from Blade with @js(...), so server state
(open on load, initial value) reaches the browser without a second render.
// resources/js/ui/dialog.js — copied by `ui:add dialog`, yours to edit.
document.addEventListener('alpine:init', () => {
window.Alpine.data('uiDialog', (config = {}) => ({
open: config.open === true,
show() {
this.open = true;
// remember the trigger, move focus into the panel, lock scroll…
},
hide() {
this.open = false;
// …restore focus to the trigger on close.
},
}));
});
Heavy dependencies load on demand
The few components that need a large library (WebGL effects use
three, charts use apexcharts) import it
dynamically inside init(). Vite splits it into its own chunk, so
pages that never mount the component never download it. Keep that pattern when you add your own.
// resources/js/ui/blob.js — a heavy dependency loads only when the component mounts.
async init() {
let THREE;
try {
THREE = await import('three'); // split into its own chunk by Vite
} catch (e) {
return; // dependency missing: empty canvas, no error
}
const renderer = new THREE.WebGLRenderer({ canvas: this.$refs.canvas, alpha: true });
// …
}
NPM dependencies
Items declare their npm requirements in dependencies.npm. The CLI prints the
install command at the end of ui:add (and in --dry-run);
it never edits package.json or runs npm for you.
php artisan ui:add blob --dry-run
npm install three
npm run build
Livewire and morphing
Behaviour modules survive Livewire re-renders and wire:navigate: state lives
in Alpine, the root keeps its x-data, and overlays clean up their listeners
on destroy(). Components that must not be morphed by a third-party library
document a wire:ignore region. Details and the tested patterns are on the
Livewire page.
Note
The docs site itself loads behaviour modules lazily (a DOM scan decides which to import before
Alpine.start()). That is a documentation-site optimisation for 800+ items;
an application with a handful of components should keep the simple static imports above.