---
title: "DOM Events and Event Handlers in LemonadeJS"
description: "Events are real functions on lowercase on* attributes: native DOM events, no synthetic layer, no string handlers, CSP-safe. One casing rule throughout."
source: https://lemonadejs.com/docs/events/
---

# DOM events and handlers

An event is an `on*` attribute holding a **function**. The handler
receives the native DOM event. That is the entire model:

```javascript
html`<button onclick="${() => save()}">Save</button>
     <input onkeydown="${(e) => handle(e)}" />`
```

Component callbacks are the same thing one level up: lowercase function
props:

```javascript
html`<${Card} onsave="${(v) => api.save(v)}" />`
```

Event naming is inconsistent across environments, and the failure mode is
expensive: a handler that attaches under the wrong name fires silently
never. LemonadeJS enforces one rule, that all event and callback names are
lowercase, to make that failure mechanically impossible.

Any `on*` attribute takes a function and receives the native event.

<!--example-->

```js
import { html } from 'lemonadejs';

const App = (props, { state }) => {
    const log = state([]);
    const add = (msg) => (log.value = [msg, ...log.value].slice(0, 5));

    return html`<div>
        <button onclick="${() => add('click: saved')}">Save</button>
        <input placeholder="type, then press Enter"
            onkeydown="${(e) => e.key === 'Enter' && add('keydown: Enter with "' + e.target.value + '"')}" />
        <div onmouseenter="${() => add('mouseenter: box')}"
             style="margin-top:8px;padding:12px;border:1px dashed #999">hover this box</div>
        <ol>${() => log.value.map((m) => html`<li>${m}</li>`)}</ol>
    </div>`;
};
```

## Mechanics

**One casing rule, no exceptions.** Every event and callback name is
lowercase, HTML-style: `onclick`, `oninput`, `onchange`, `onsave`,
`onitemclick`, exactly how the platform spells `onmousedown`. Any other
casing warns `LJS-305` in dev, on elements *and* on component props. On
native elements the event name is normalized so a stray `onMouseDown`
still attaches (and warns); on components, where a wrong-cased prop would
be silently ignored, the tripwire is the whole point.

**Functions only, enforced.** An `on*` attribute that does not hold a
function fails hard with `LJS-301` at mount, in dev and production alike.
String handlers do not exist, so templates are CSP-safe by construction,
and since the engine contains no `eval` or `new Function` anywhere, the
zero-build deployment runs under a strict CSP.

**Native events, no synthetic layer.** The handler receives the browser's
event object. `onchange` is the DOM `change` event; `oninput` is `input`.
What MDN documents is what happens.

**Attach once, resolve per dispatch.** The engine adds one listener per
`on*` attribute and reads the current handler through the template's value
holder at dispatch time, so when a branch re-renders with a new closure,
the handler is swapped without any `removeEventListener`/`addEventListener`
churn.

## Component callbacks

A component event is nothing more than a lowercase function prop:
declared in the [contract](/docs/contracts/) as an event:

```javascript
export const Datagrid = component('datagrid', {
    // ...
    onchange: Function,           // (row, columnName, value, oldValue)
    onselect: Function,           // (selectedRows)
    onsort: Function,             // (columnName, direction | null)
    oncolumnresize: Function,     // (columnName, widthPx) on handle release
}, (props) => { /* ... */ });
```

The declaration buys every deployment at once: inside LemonadeJS the
callback is called directly; in the custom-element deployment each
declared event dispatches a real `CustomEvent` (bubbling, composed) named
after the prop minus `on`, so `addEventListener('change', ...)`, Vue's
`@change`, Angular's `(change)` all just work, and the React adapter maps
it back to a React-style callback. Passing a non-function for a declared
event warns `LJS-401`.

For the two-way `onchange` protocol (`value.set()` commits *and* fires
`onchange`; parent writes never echo back), see
[Two-way binding](/docs/two-way-binding/). And anything that closes or
commits on `focusout` should check `isDisposing()`, because the renderer
removing a focused element fires `focusout` exactly like a user leaving;
see [Destroy](/docs/destroy/).

## In practice

The Tabs block header (`components/tabs`, shipped source): one `<ul>`,
seven native handlers, all plain functions: click, focus-follows-arrows,
keyboard, and drag-sorting:

```javascript
<ul ref="${(el) => (ul = el)}"
    onclick="${onOpenEvent}"
    onfocusin="${onOpenEvent}"
    onkeydown="${onKeydown}"
    ondragstart="${onDragstart}"
    ondragover="${(e) => e.preventDefault()}"
    ondrop="${onDrop}"
    ondragend="${onDragend}">
```

And the Switch block shows both layers in one line each: the native
`change` event of its real `<input type="checkbox">` feeds the bound
state, and the *component's* `onchange` fires only for user-initiated
commits, because the commit goes through `set()`:

```javascript
<input type="checkbox" class="lm-switch-input"
    onchange="${(e) => current.set(e.target.checked)}" />
```

A parent writing `enabled.value = true` programmatically updates the
switch without echoing `onchange` back, the protocol that prevents
update loops between parent and child.

## Reference

```javascript
onclick="${fn}"            // any on* attribute; fn receives the native event
on*  → function, always    // LJS-301 (throws): strings never
onclick, onchange, onsave  // lowercase, always; LJS-305 warns otherwise
<${C} onsave="${fn}" />    // component callback: a lowercase function prop
onsave: Function           // contract declaration → CustomEvent 'save' on <lm-c>
```

- Handlers are attached once per attribute; branch updates swap the
  function without re-attaching the listener.
- Declared contract events must be lowercase (they become HTML-facing
  names); non-function values warn `LJS-401`.
- `isDisposing()` distinguishes renderer-caused `focusout` from the user's.