Plugin DOM events
Toolkit packages expose observable behavior to Alpine templates through
namespaced DOM events. Consumers listen with Alpine’s @ syntax; the
underlying event name is package:event.
<div @toggle:change="handleToggle($event.detail)" @dialog:before-close="validateClose($event)" @theme:change.window="syncTheme($event.detail)"></div>The
@package:eventconvention is a naming and dispatch contract, not a helper. Packages dispatch plainCustomEventinstances with the namespacedpackage:eventtype. There is no shareddispatchPluginEvent()helper anymore — every package builds the event itself so its tree-shaking footprint stays predictable.
Naming rules
Section titled “Naming rules”| Rule | Example |
|---|---|
Toolkit events use @package:event |
@menu:activate, @selection:change |
DOM event name is package:event |
menu:activate, selection:change |
| Segments are lowercase kebab-case | before-close, not beforeClose |
Pre-action cancelable events use before-* |
@dialog:before-close |
| Package namespace matches the public capability | toggle, dialog, theme |
| Native browser events stay unnamespaced | @play, @focus, @scroll |
Do not emit namespaced events that duplicate native DOM events:
<!-- Avoid -->@media:play@scroll:scrollA namespaced event is justified only when it represents toolkit-specific state or lifecycle not already represented by a native event.
Dispatch shape
Section titled “Dispatch shape”const event = new CustomEvent<DialogCloseDetail>("dialog:close", { detail: { previous: true, current: false, source: "api" }, bubbles: true, composed: true,});
element.dispatchEvent(event);Defaults
Section titled “Defaults”| Option | Recommended default |
|---|---|
bubbles |
true |
composed |
true |
cancelable |
false |
Cancelable lifecycle hooks opt in explicitly:
const event = new CustomEvent<DialogBeforeCloseDetail>("dialog:before-close", { detail: { reason: "user" }, bubbles: true, composed: true, cancelable: true,});element.dispatchEvent(event);
if (event.defaultPrevented) { return false;}Event detail contract
Section titled “Event detail contract”Each package defines explicit detail types:
interface ToggleChangeDetail { previous: boolean; current: boolean; source: "on" | "off" | "toggle" | "external";}Guidelines:
- Include only stable public data in
detail. - Avoid ambiguous payloads such as
{ value: true }when semantics matter. - Do not pass internal controller references unless clearly justified.
- Event detail objects are cloned before dispatch — callers may reuse or mutate their original object after dispatch.
Packages are encouraged to expose a normalized source union when the
origin matters:
type ChangeSource = "api" | "keyboard" | "pointer" | "external" | "system";TypeScript augmentation
Section titled “TypeScript augmentation”Each package declares its event names locally — there is no shared
PluginEventMap to augment. Inline the type alongside the dispatch
site:
type DialogEventMap = { "dialog:close": DialogCloseDetail; "dialog:before-close": DialogBeforeCloseDetail;};
const event = new CustomEvent<DialogCloseDetail>("dialog:close", { detail: { previous: true, current: false, source: "api" }, bubbles: true, composed: true,});If a consumer wants to type a listener, they can use
CustomEvent<DialogCloseDetail> directly — no global augmentation
required.
Dispatch targets
Section titled “Dispatch targets”| Context | Target |
|---|---|
| Element-bound directive | Owning element |
| Global store change | window (listen with .window) |
| Unattached controller factory | Do not emit until Alpine wires a DOM owner |
Examples:
<div @theme:change.window="syncTheme($event.detail)"><div x-dialog @dialog:close="restoreState()">Packages must not create hidden global event buses when native event propagation is sufficient.
Documentation requirements
Section titled “Documentation requirements”Public package docs must show Alpine listener syntax with @:
@menu:activate@selection:changeDocument for every event:
- Listener syntax (
@package:event, including.windowwhen relevant) - Dispatch target
- Bubbling and composition behavior
- Cancelability
- Detail type and example payload
- Exact moment the event fires
- Whether external state changes can trigger it
When not to add an event
Section titled “When not to add an event”Skip a new DOM event when:
- A native DOM event already covers the behavior.
- The event would fire for every controller method instead of meaningful observable transitions.
- You would need a separate global bus instead of
EventTargetpropagation. - The payload cannot be described with a stable public detail type.
Toolkit events complement Alpine’s $dispatch. Use $dispatch when
you want to fire a one-off custom event from inside an Alpine template;
use new CustomEvent("package:event", …) for toolkit-standard
namespaced events from controllers, stores, and directives.
Testing checklist
Section titled “Testing checklist”- Default bubbling and composition
- Cancelable
before-*events anddefaultPrevented - Typed detail payloads
- Dispatch from elements and
window - Supplied detail is not mutated
- SSR-safe module import
- Repeated dispatches are independent
- Alpine listeners receive events via
@package:eventand.window
Related
Section titled “Related”packages/core/README.md— API referenceAGENTS.md— monorepo conventions
