Browse documentation

Template bindings / Reference

Attributes, properties, and boolean state

Choose the exact browser field a scalar interpolation owns, including sparse attributes and boolean presence.

A search input can have value="scope" in its HTML while its visible text says events. That is normal: the attribute describes markup, while the property is the live edit buffer. A disabled button adds another distinction—its boolean attribute is true by being present, even if its text is "false".

After authoring a template, use this compact lookup when a binding must choose an exact browser field.

BindingInspectClearing behavior
title=${value}getAttribute("title")nullish removes the attribute
title="Search: ${value}"the complete joined attributenullish segment becomes empty text
.value=${value}input.valueassigns the supplied value directly
?disabled=${value}hasAttribute("disabled")falsy removes presence

Use attributes for serialized metadata

An ordinary name=${value} part sets one attribute. Non-nullish values become strings; null and undefined remove the attribute. false becomes "false", which is useful for an ARIA state but is not a removal signal.

import { html } from "@typed/template";

const description: string | null = "Search titles and descriptions";

export const field = html`<input
  type="search"
  aria-label="Search saved articles"
  title=${description}
/>`;

If description is a live producer instead of this snapshot, its emissions update the same attribute. No input replacement is needed. Choose this form for metadata such as aria-*, id, role, href, and title when that attribute is the surface you mean.

A sparse expression such as title="Search: ${description}" is different. It joins literal and dynamic segments; a nullish segment becomes empty text but does not remove the whole attribute. Use a full attribute part when absence itself is meaningful.

Use properties for current browser state

A leading dot means direct assignment to the element object. It does not stringify the value and does not treat nullish values as an instruction to remove an attribute.

import { RefSubject } from "@typed/fx";
import { component, html } from "@typed/template";
import * as EventHandler from "@typed/template/EventHandler";

export const QueryField = component(function* () {
  const query = yield* RefSubject.make("scope");

  const readQuery = EventHandler.make((event: Event) =>
    RefSubject.set(query, (event.currentTarget as HTMLInputElement).value),
  );

  return html`<input value="scope" .value=${query} oninput=${readQuery} />`;
});

The attribute provides initial serialized text. .value makes application state the writer of the current property, and oninput records user edits back into that state. The browser’s value property documentation describes the live value you read here.

This does not create a generic two-way binding: the event and state update are explicit. If the application later assigns a different value, it is intentionally replacing the current edit buffer. Selection policy, validation, and request timing remain application decisions.

DOM-only fields such as .indeterminate also belong to properties. Server HTML has no generic representation for property assignments; an initial .value alone does not serialize value=. Use an authored attribute where the initial response needs one, and let client setup apply the property when it starts.

Use boolean parts when presence means true

?disabled=${value} toggles the attribute according to JavaScript truthiness:

import { RefSubject } from "@typed/fx";
import { component, html } from "@typed/template";

export const SaveControl = component(function* () {
  const readOnly = yield* RefSubject.make(false);

  return html`<button type="button" ?disabled=${readOnly}>Save search</button>`;
});

false, null, undefined, 0, and an empty string remove the attribute. A nonempty string, including "false", adds it. Pass a boolean when that is your domain meaning rather than relying on an accidental string conversion.

Do not use the boolean form for aria-expanded: assistive technology needs an attribute string representing true or false, not a presence-only HTML boolean. aria-expanded=${expanded} and ?disabled=${disabled} intentionally use different forms.

Inspect the field that the binding actually owns

When DevTools shows a surprising result, compare the attribute and property. An attribute mutation observer cannot tell you whether a property was written. See DOM scalar parts and attributes for diagnosis, or Spread props and data records to group bindings.