_ bindings

_ followed by an attribute name binds that attribute to a reference. The binding is one-way: it follows the value whenever it changes. Used on any element in component markup.

<element _name="#ref"></element>
<element _name="!#ref"></element>
<element _--custom-property="#ref"></element>
<a _href="#link.url">Open</a>
<section _hidden="!#panel.open">…</section>
<button _disabled="@disabled">Save</button>
<input _value="#form.email">
<user-card _user="#session.user"></user-card>
<div _--progress="#upload.percent"></div>

The _name attribute itself is removed when the markup is processed. The value must be a reference. Anything else logs a warning and the binding is skipped.

How values are applied

The first matching rule wins:

Target Value Result
_--name null, undefined, false style.removeProperty("--name")
_--name anything else style.setProperty("--name", String(value))
value on <progress> or <meter> null, undefined Attribute removed, so the element stays indeterminate
value, checked, selected, indeterminate on an element with that property any Sets the property. value gets value ?? "". The others get Boolean(value)
Any attribute of a custom element (tag with a dash) object or array Sets the camelCase property (_user-data sets userData), so the object arrives intact
aria-*, draggable, spellcheck, contenteditable true, false Attribute set to "true" or "false"
Any attribute true Attribute set to ""
Any attribute false, null, undefined Attribute removed
Any attribute anything else Attribute set to String(value)

On SVG elements, the attribute name's case is restored for camelCase SVG attributes such as viewBox, preserveAspectRatio, gradientTransform, stdDeviation and textLength, so _viewbox and _viewBox both work.

Changes come back through events

Bindings never write back to state. Update state from a handler:

<input _value="#todo.draft" oninput="todo.draft = this.value">

class

_class replaces the whole class attribute. To toggle one style, bind a data attribute and select it in CSS (_data-active="#tab.active"), or use a reactive :state().

Example

Previewindex.htmlvolume-meter.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>_ bindings demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="./volume-meter.html"></import-element>

  <volume-meter></volume-meter>
</body>
</html>
<custom-element name="volume-meter">
  <script>
    const level = useState({
      value: 40,
      get loud() {
        return this.value > 70;
      },
    });
  </script>

  <style>
    :host {
      display: grid;
      gap: calc(var(--spacing) * 2);
      width: min(calc(var(--spacing) * 80), 100%);
    }
    .bar {
      height: calc(var(--spacing) * 2);
      width: calc(var(--level) * 1%);
      border-radius: var(--radius-full);
      background: var(--primary);
    }
    .bar[data-loud] {
      background: var(--destructive);
    }
  </style>

  <input type="range" min="0" max="100" aria-label="Volume" _value="#level.value" oninput="level.value = Number(this.value)">
  <div class="bar" _--level="#level.value" _data-loud="#level.loud" _aria-label="#level.value" role="img"></div>
</custom-element>

Edit this page on GitHub · View as Markdown