This page explains the runtime's DOM-first events model and practical guidance for emitting and handling events across framework boundaries.
- Components should emit standard DOM CustomEvents. Use the
useEmit()hook or dispatch aCustomEventdirectly from the component. Events should generally setbubbles: trueandcomposed: trueso they cross shadow boundaries and are discoverable by host frameworks. - Hosts and framework integrations should listen using normal DOM or framework
bindings (e.g.,
@eventin Vue,on:eventin Svelte,(event)in Angular, oraddEventListeneron refs for React).
Use the useEmit() hook inside your component function to get the emit function:
// In your component function
component('my-component', () => {
const props = useProps({ title: 'Save' });
const emit = useEmit();
const handleSave = () => {
emit('save', { id: 123, title: props.title });
};
return html`<button @click="${handleSave}">${props.title}</button>`;
});API note: emit(name, detail?, options?) forwards options to the underlying CustomEvent constructor (for example { cancelable: true, bubbles: true, composed: true }) and returns a boolean indicating whether the event was not prevented (i.e., true == not defaultPrevented).
Recommendation: always use bubbles: true and composed: true for events that
need to reach host pages or framework templates.
Some events represent requests ("please close", "please delete") where the host
may want to veto the action (for example, unsaved changes). In those cases emit a
cancelable CustomEvent and check whether the host called preventDefault().
How to emit a cancelable event from a component:
// inside component logic
component('closable-modal', () => {
const props = useProps({
onClose: undefined as unknown as (() => void) | undefined,
});
const emit = useEmit();
const handleCloseRequest = () => {
// emit a cancelable event and get a boolean result indicating whether the
// event was *not* defaultPrevented (true == allowed)
const allowed = emit('close', { reason: 'user' }, { cancelable: true });
if (!allowed) {
// the host prevented the close by calling event.preventDefault()
return; // abort closing
}
// proceed to close the component
props.onClose?.();
};
return html`
<div class="modal">
<button @click="${handleCloseRequest}">Close</button>
</div>
`;
});How a host can prevent the action:
const modal = document.querySelector('closable-modal');
modal.addEventListener('close', (ev) => {
if (hasUnsavedChanges()) {
ev.preventDefault(); // veto the close
}
});Notes and best practices:
- Call
emit(name, detail, { cancelable: true })only when the action is logically a request that the host might legitimately block. Most events are simple notifications and shouldn't be cancelable by default. - The runtime helper
emitreturnstruewhen the event was not prevented (convenient for immediate checks). Always check this return value before continuing with an action that the host can veto. - Parser caveat: some host framework template parsers (or strict linters) may
have difficulty with event attribute names that include
:characters (for example(update:model-value)or@update:model-value) in certain syntaxes. - If you encounter parse/compile errors in a host framework, prefer using plain
addEventListeneron a ref or use hyphenated event names and listen imperatively. - Avoid making every event cancelable — prefer explicit cancelable events for clear intent and fewer accidental interactions.
- Vue:
<my-comp @save="onSave" />— handler receives a DOM CustomEvent; payload ise.detail. - Svelte:
<my-comp on:save={onSave} />— handler receives a DOM CustomEvent. - Angular:
<my-comp (save)="onSave($event)"></my-comp>— handler receives the event object. - React: use a ref and
addEventListeneron the element instance to listen for CustomEvents.
If you need to attach programmatic handlers to capture closure state on the host,
get a reference to the element and add/remove listeners using addEventListener.
Child component (emits a semantic event):
component('item-list', () => {
const props = useProps({ items: [] as Array<{ name: string }> });
const emit = useEmit();
const handleItemClick = (item: { name: string }) => {
emit('item-selected', { item });
};
return html`
${each(
props.items,
(item) => html`
<div @click="${() => handleItemClick(item)}">${item.name}</div>
`,
)}
`;
});Parent template (listens for the semantic event):
<item-list @item-selected="${ctx.handleItemSelected}"></item-list>// parent handler
function handleItemSelected(ev: Event) {
// CustomEvent payload is in ev.detail
if ('detail' in ev) console.log('item selected', (ev as CustomEvent).detail);
}Recommendation: avoid reusing native event names (like click) for semantic payload events; use descriptive names such as open, save, or activate. Reusing native event names can cause ambiguity about whether the native browser event or a custom event is firing.
- Prefer DOM CustomEvents for cross-framework communication.
- Framework adapters should translate framework listener syntaxes to native DOM listeners for CustomEvents dispatched by components.
- Avoid coupling component internals to host implementation details; expose a clear, event-driven interface instead.
- For two-way binding compatibility (:model and our compiler transforms), components should emit kebab-cased
update:<prop-name>CustomEvents (for exampleupdate:model-valueorupdate:some-prop). - The runtime and framework adapters expect the new value to be the event payload (the raw value) available on
event.detail. - Emit events with
bubbles: trueandcomposed: trueso they traverse Shadow DOM boundaries and are discoverable by host templates and framework bindings.
-
When writing components:
- Use
useEmit()inside your component (orcontext.emit(name, detail)when you have the context) to notify outside consumers. - Set
bubbles: trueandcomposed: truewhen events need to cross shadow boundaries.
- Use
-
When writing host code or framework adapters:
- Use the framework's event binding where available.
- When you need closure capture or imperative wiring, use refs and
addEventListener.
functional-api.md,cross-component-communication.md, and framework integration guides for concrete examples.