Documents v0.9.0 · previews run the real runtime · experimental features are marked exp

Form validation

A sign-up form using native constraint validation: required fields, type="email", minlength. A company field appears and becomes required only for business accounts. Submit is disabled until the terms are accepted, and error messages appear after the first submit attempt. Nothing is sent.

Live previewreal runtime
View source

How it works

  • bind:lines 3, 4Connects a native form control two-way with the nearest state of that name.
  • attr:lines 9, 10, 12Derives one allowlisted HTML attribute of the matched element, such as disabled or aria-expanded, from a reactive value.
  • set()line 12Sets a state to a value. In an :on() rule it is a mutation of the state on the left, --x: set(5), with typed expressions.
  • :when()lines 15, 16, 17Matches elements for which the condition is true, with states resolved from that element.

The browser does the validation. Reactive CSS only changes which constraints apply: a derived required and hidden on the conditional field, and a derived disabled on submit. Messages are native :invalid styling.

Left to native HTML and CSS

  • Validation: required, type="email", minlength, :invalid, :valid, the browser’s own validation bubble and focus on the first invalid field.
  • No submission: method="dialog" on a form outside a <dialog> does nothing on a valid submit (HTML spec), so the example sends nothing without script.

What would normally need JavaScript

Making a field conditionally required, and disabling submit until the terms are accepted. CSS can style :invalid, but it cannot change which constraints apply.

Limitations

  • Messages are static text. CSS cannot read validationMessage; each field has one fixed message. Fields with several failure reasons (too short and wrong format) get one message.
  • When messages appear. After the first submit click (--tried). Native :user-invalid is an alternative where supported; it is not used, so both engines behave the same.
  • Disabled submit hides the reason. A disabled button cannot be clicked, so the browser never shows its validation bubble until the terms are accepted. This is a design choice of the example, not a requirement.
  • Hidden fields keep their value. Switching to Personal hides the company field but keeps its text, which is still form data. disabled on the field would drop it from submission; hiding was chosen so switching back keeps the value.
  • No server. Client-side validation is a convenience; the server must validate again.

Accessibility

  • Labels, aria-describedby on every field, visible focus, native error bubbles.
  • required is real, so assistive technology announces it, including on the company field when it appears (tested).
  • Static messages are not live regions; the success message is role="status". Not tested with screen readers.
  • attr: does not make the form accessible by itself: the labels, descriptions and native validation do.

Implementation

2 files · 20 lines
style.cssreactive
/* ---- state ------------------------------------------------------------------------------------ */.signup { --type: state(personal); --terms: state(false); --tried: state(false); }.signup .type input { bind: --type; }.signup .terms input { bind: --terms; }/* ---- derived attributes (attr:, 0.5.3) ---------------------------------------------------------   The company field exists only for business accounts. It is hidden AND not required otherwise:   a hidden required field would block submission with no visible message. */.signup .company { attr: hidden when(--type == business, false, true); }.signup .company input { attr: required when(--type == business); }/* Submit is natively disabled until the terms are accepted. */.signup button[type=submit] { attr: disabled when(--terms, false, true); on: click(set(--tried, true)); }/* ---- messages: native :invalid / :valid, shown after the first submit attempt --------------- */.signup:when(--tried) .field:has(input:invalid) .err { display: block; }.signup:when(--tried) .field input:invalid { border-color: #b42318; }.signup:when(--tried):valid .ok { display: block; }/* Real DOM text (text:, 0.5.1): the account type in words. */.signup .kind { text: when(--type == business, "Business", "Personal"); }
state()Function · hover any red tokenMakes each matching element the owner of an independent state with an initial value.Reference →

Source: the library's examples/form-validation/. style.css from its first section comment; the layout above it runs but is not shown. Written in the 0.8 syntax (on:, bind:, text:, attr:), which 0.9.0 still runs; see Migrating from 0.8 for the 0.9 forms.

Related

Focus transitions

Three panels switched by buttons, Previous/Next and a native popover menu. Focus follows user-initiated panel changes to the new panel's heading (or to Confirm on panel 3); a hidden section is revealed and focused in one action; a locked (disabled) Confirm and the open popover deliberately keep focus where it is.

0.8 syntaxset()increment()decrement()toggle()

Keyboard navigation

A listbox-like list on a deliberately tall page. Arrow keys, Home and End move the selection and focus (roving tabindex) and are cancelled with prevent(), so the page does not scroll; a lock makes the list's key rule inactive, and the keys scroll the page again. A nested region inside one item has its own keys, and a filter field in the same section keeps its native keys.

0.8 syntaxkey()prevent():when()cycle()

Attribute playground

State controls on one side, targets with derived attributes on the other, and a live table that shows the real DOM attribute values (read with CSS :has() selectors, no script).

0.8 syntaxattr:bind:on::when()