---
title: "Reactive Attributes in LemonadeJS"
description: "Attribute bindings rebuild the whole attribute string and write it atomically, so a partial update is impossible. Plus the property-when-exists rule."
source: https://lemonadejs.com/docs/attributes/
---

# Reactive attributes

Attributes are reactive by default. Any `${...}` inside one keeps the whole
attribute live:

```javascript
html`<div class="btn ${active}">                 <!-- mixed static + state -->
     <input value="${name}" />                   <!-- set as a property -->
     <input type="checkbox" checked="${on}" />   <!-- boolean property -->
     <input disabled />                          <!-- bare boolean attribute -->
     <lm-datagrid data="${rows}"></lm-datagrid>` <!-- object → element PROPERTY -->
```

One binding per attribute; one write per change; the full value rebuilt
every time. Nothing in between.

Mixed class parts, boolean properties and the optional-attribute idiom (`|| false` removes it).

<!--example-->

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

const App = (props, { state }) => {
    const active = state(false);
    const locked = state(false);
    const size = state('');

    return html`<div>
        <button class="btn ${() => (active.value ? 'active' : '')}"
            disabled="${locked}"
            data-size="${() => size.value || false}"
            style="${() => (active.value ? 'background:#ffd43b' : '')}"
            onclick="${() => (active.value = !active.value)}">
            ${() => (active.value ? 'active' : 'inactive')}
        </button>
        <label><input type="checkbox" bind="${locked}" /> disabled</label>
        <select bind="${size}">
            <option value="">no data-size</option>
            <option value="small">data-size="small"</option>
            <option value="large">data-size="large"</option>
        </select>
        <p><small>Inspect the button in devtools: class, disabled and data-size follow the states.</small></p>
    </div>`;
};
```

## Background

Attributes sit at the intersection of two long-standing platform
complications:

- **The attribute/property split.** HTML attributes are strings; DOM
  properties are typed and live. `value` the attribute is the *initial*
  value; `value` the property is the current one. `checked`, `disabled`,
  `selected` all have this dual nature, and each requires a consistent
  resolution rule.
- **Partial updates.** Systems that update interpolated regions *inside*
  an attribute string can half-update it. LemonadeJS v5 had exactly this
  wound: a mixed attribute like `class="test ${x}"` could mis-concatenate
  on update, leaving stale and new fragments combined. Bugs of this class
  are maddening because the template reads correctly: the corruption
  exists only at runtime, after the *second* update.
- **Rich data and custom elements.** Attributes carry strings, so passing
  an array of 100,000 rows as an attribute is not possible, so someone has
  to choose the property path. Contract-built custom elements define
  property accessors for every declared prop so structured data crosses
  the boundary without serialization.

## Mechanics

**One binding per attribute, atomic writes.** Each attribute with
expressions owns exactly one binding. When a state it reads changes, the
binding re-runs: the full value is rebuilt from *all* its parts. The
static strings are immutable parser output, the dynamic parts are read
fresh. It is compared with the last value (`Object.is`), and written in a
single assignment. The template never re-runs, no other binding runs, and
there is **no code path that patches a substring of an attribute**. The
v5 concatenation bug is not fixed; it is structurally impossible.

**Removal and presence have one rule.** `false`, `null` and `undefined`
**remove** the attribute (and reset a same-named boolean property);
`true` sets it present. The idiom for optional attributes is
`${() => cond.value || false}`: render it or remove it, never `"null"`
as text.

**Property when one exists.** If the element has a same-named property
(`value`, `checked`, `disabled`), the property is set, so form elements
behave as a user expects (the *current* value changes, not the HTML
default). `class` and `style` always go through `setAttribute`, and SVG
always uses attributes, because that is what the platform defines there.

**Objects and functions become element properties, always.** A
whole-value expression resolving to an object or function is assigned as
a property, never stringified:

```javascript
const rows = store([...]);   // 100k row objects

html`<lm-datagrid data="${rows}"></lm-datagrid>`;
// → el.data = rows.value, the ARRAY, by reference. No JSON, no
//   stringify, identity preserved.
```

This is how data crosses into the custom-element deployment: a
contract-built element defines a property accessor for every declared
prop, so `el.data = rows` lands in the component's live `data` state and
the grid updates. Assignments cross this boundary; for in-place mutation
plus `touch()` semantics, compose inside LemonadeJS
(`<${Datagrid} data="${rows}" />`), where the state itself is passed by
reference. See [Components](/docs/components/) and
[Web components](/docs/web-components/).

**Three attribute names are directives, not attributes.** `bind`, `ref`
and `on*` are consumed by the engine and never rendered to the DOM:
`bind="${state}"` wires two-way form binding (`LJS-302`–`LJS-304`
validate it; see [Two-way binding](/docs/two-way-binding/)), `ref`
receives the created element, and `on*` attaches handlers
(see [Events](/docs/events/)).

## The data-* convention

Published Studio blocks expose **state** as `lm-<name>-*` classes and
**styling variants** as valid `data-*` attributes, never invented bare
attributes that would fail HTML validation. The Switch root, shipped
source (`components/switch`):

```javascript
return html`<label
    class="lm-switch ${() => (current.value ? 'lm-switch-on' : 'lm-switch-off')} ${() =>
        props.disabled!.value ? 'lm-switch-disabled' : ''} ${() =>
        props.size!.value ? 'lm-switch-' + props.size!.value : ''}"
    data-position="${() => props.position!.value || false}"
    data-color="${() => props.color!.value || false}">
```

The class attribute mixes one static part and three live expressions:
one binding, rebuilt whole on any change, so the combinations can never
interleave. The variants are CSS-addressable as
`[data-color='red']`, `[data-position='right']`, and the `|| false`
idiom removes the attribute entirely when the prop is unset.

## In practice

Two ends of the same mechanism, both from shipped blocks:

**Live styling, the Switch** (above): four dynamic attributes on one
element (a mixed `class`, two `data-*` variants), each its own binding.
Toggling the switch re-runs exactly one of them (the class), rebuilds one
string, performs one DOM write. The probe suites assert the rendered
attribute values, which only works because there is no intermediate
half-state to race against.

**Rich data, the datagrid**: the demo passes 100,000 row objects through
the `data` attribute position. Inside LemonadeJS the state crosses by
reference and `rows.touch()` re-renders the visible window; through
`<lm-datagrid>` the same array lands as an element property assignment.
Either way, zero copies and zero serialization. The receipts are in
[State](/docs/state/).

## Reference

```javascript
class="a ${x} ${() => y.value}"  // mixed parts → ONE binding, atomic rebuild
value="${name}"                  // property when the element has one
checked="${on}"  disabled        // booleans: true = present, false/null/undefined = removed
attr="${() => v.value || false}" // optional-attribute idiom
data="${objOrFn}"                // objects/functions → element PROPERTY, never stringified
```

- `class`, `style` and all SVG attributes go through `setAttribute`.
- Identical resolved values are skipped before any DOM write (`Object.is`).
- `bind`, `ref` and `on*` are engine directives: consumed, never rendered.
- Styling conventions for published blocks: `lm-<name>-*` classes for
  state, valid `data-*` attributes for variants.