Skip to content
Home Theme Gallery

Field

The custom element is <hui-field>.

Overview

A light-DOM wrapper that wires a rendered label, description and error to one control. It has no shadow root and no focus of its own, and it re-wires through a MutationObserver so an HTMX swap that adds an error message is picked up automatically.

Example

Valid and invalid
Source
<hui-field>
<span data-label>Name</span>
<hui-input data-control value="Ada"></hui-input>
<span data-description>Your full name</span>
</hui-field>
<hui-field invalid>
<span data-label>Code</span>
<hui-input data-control value="ab" pattern="[0-9]{3}" required></hui-input>
<span data-description>Three digits</span>
<span data-error>Must be three digits</span>
</hui-field>

The Go template that renders it:

<hui-field {{ if .Invalid }}invalid{{ end }}>
<span data-label>{{ .Label }}</span>
<hui-input data-control name="code" value="{{ .Value }}" required></hui-input>
<span data-description>Three digits</span>
{{ if .Error }}<span data-error>{{ .Error }}</span>{{ end }}
</hui-field>

API

Attributes2
Attributes
NameTypeDefaultDescription
invalidbooleanfalseMarks the wrapped control aria-invalid="true". Reflected.
requiredbooleanfalseMarks the wrapped control aria-required="true". Reflected.
Properties2
Properties
NameTypeDefaultDescription
invalidbooleanfalseReflects to the invalid attribute.
requiredbooleanfalseReflects to the required attribute.

States

Required

required marks the label and passes the constraint to the control the field wraps.

Required
Source
<hui-field required>
<span data-label>Email</span>
<hui-input data-control name="email" type="email" placeholder="you@example.com"></hui-input>
</hui-field>

Server-side mechanics

hui-field owns no form value; it is a light-DOM wrapper the server renders around a control, using the data-label, data-control, data-description and data-error child hooks to wire the control's accessible name and description. The server owns the invalid and required attributes and the text of those children, and the wrapped control submits under its own name, not the field's. An hx-swap that adds or replaces an error message is picked up automatically: a MutationObserver re-wires the field on child, text and attribute changes. There is no field-level validation of its own, so invalid only forwards aria-invalid="true" to the control.

Accessibility

  • A child marked data-label supplies the control accessible name through aria-label and aria-labelledby.
  • Children marked data-description and data-error are joined into aria-description, and their generated ids become aria-describedby and aria-errormessage.
  • A data-error that is hidden or empty is not an error yet: it is not drawn, glyph included, and not wired to the control until it has a message. A host can render the slot once and fill or reveal it later.
  • The description text precedes the error text in the accessible description, matching the rendered order.
  • Because it is light DOM, the IDREF attributes it sets resolve for both native controls and the shadow controls, whose internal inputs receive the text through ARIA forwarding.
  • A MutationObserver re-wires the field on child, text or attribute changes, so a swapped-in error message is announced without extra JavaScript.

Keyboard

Keyboard
KeysAction
TabMoves focus straight to the wrapped control; the field wrapper is never a tab stop.
Control keysEvery key is handled by the wrapped control; hui-field adds no key handling of its own.

Gotchas

  • It is light DOM with no shadow root: createRenderRoot returns the host, so its contract is the data-label, data-control, data-description and data-error child hooks rather than any shadow parts.