Native HTML forms, progressively enhanced.
Combat UI keeps real <form>, <input>, <select>, and
<textarea> elements in light DOM. CSS classes style them; cui-field wires
labels, help, errors, and ARIA; cui-form orchestrates validation and submit. Strings — labels,
help, error messages — stay yours and drop in as slots.
Quick start
Put a native <form> inside <cui-form>, wrap each control in
<cui-field>, and style controls with the .cui-input family. The
cui-field wrapper handles ARIA wiring, :user-invalid styling, and error
rendering. cui-form blocks submission until validation passes and exposes the
FormData to your handler.
Form classes
The .cui-input family styles native <input>,
<select>, and <textarea> elements. They work standalone — no
JavaScript required — and integrate with cui-field for validity affordances. CSS-only
forms still get correct :user-invalid styling.
cui-field
cui-field wraps a single native control and owns the surrounding affordances: a
clickable label, descriptive help, an ARIA-live error region, aria-describedby,
aria-invalid, and aria-required. It listens for blur and
invalid and runs the validation pipeline against the slotted control.
Errors only render after the field is touched — a blur, an invalid event, or a form
submission attempt. That matches the platform's :user-invalid behavior and avoids
shouting at people mid-typing.
| Slot | Purpose |
|---|---|
| (default) | A single <input>, <select>, or
<textarea>. |
label |
Wrapped in a shadow <label for="…">; clicking focuses the control. |
help |
Descriptive text. ID is auto-generated and referenced from aria-describedby. |
error-valueMissing · error-typeMismatch ·
error-patternMismatch · error-tooShort · error-tooLong ·
error-rangeUnderflow · error-rangeOverflow ·
error-stepMismatch · error-badInput · error-customError
|
One pre-localized message per ValidityState flag. Wins over the generic slot when
its key matches. |
error |
Generic error display. Fill its content from a cui-field-invalid listener. |
| Attribute / property | Purpose |
|---|---|
required |
Forwards required + aria-required to the slotted control. |
data-invalid |
Set automatically when an error is shown. CSS hook. |
field.control |
Reference to the slotted control element. |
field.name |
The control's name attribute. |
field.addValidator(fn) |
Register a sync or async validator. Returns an unsubscribe function. Signature:
(value, control) => string | null | Promise<string|null>.
|
field.setError(msg) |
Show a server- or otherwise-sourced error string. Pass null to clear. |
field.markTouched() |
Force the touched state so errors render immediately. |
field.reset() |
Clears the touched state and any custom validity. |
field.validate() |
Returns Promise<boolean>. Runs native checks then registered validators.
|
| Event | Detail |
|---|---|
cui-field-invalid |
{ key, message, params: { min, max, type }, validity, control }. Use to render
translated strings into slot="error". |
cui-field-valid |
{ control }. Fired when an invalid field becomes valid. |
cui-form
cui-form wraps a native <form> and intercepts its
submit event. On submit it marks every contained cui-field as touched,
runs field.validate() in parallel, focuses the first invalid one if any fail, otherwise
dispatches a cancelable cui-form-submit event and awaits the registered submit handler
with the busy attribute toggled.
| Attribute / property | Purpose |
|---|---|
busy |
Set while the submit handler is awaited. Dims pointer events. |
form.setSubmitHandler(fn) |
Register an async handler: (data, form) => void | Promise<void>. |
form.validate() |
Promise<boolean>. Runs every field's validate in parallel. |
form.reset() |
Resets the native form and clears each field's error state. |
form.fields() |
Returns the contained cui-field elements. |
form.formElement() |
Returns the contained native <form>. |
| Event | Detail / behavior |
|---|---|
cui-form-submit |
Cancelable. { data, form }. Call preventDefault() to skip the
registered handler. |
cui-form-success |
Fired after the handler resolves. { data, form }. |
cui-form-error |
Fired if the handler throws. { error, form }. |
cui-form-invalid |
Fired when validation blocks submission. |
Validation pipeline
Three layers run, in order, per field:
-
Native constraints —
required,type=email|url|number…,pattern,min/max,minlength/maxlength,step. Surfaced viaHTMLInputElement.validity. -
Custom validators — anything registered with
field.addValidator(fn). Run sequentially after native checks pass. The first one to return a non-null string fails the field and surfaces that string via the platform'scustomErrorflag. -
External errors —
field.setError("…")for server-side or otherwise externally sourced errors.field.setError(null)clears.
Errors & i18n via slots
Combat UI does not ship an i18n provider. Strings stay with your i18n framework.
cui-field resolves the visible message in this order:
-
A per-key error slot matching the failing
ValidityStateflag —slot="error-valueMissing",slot="error-typeMismatch", etc. -
A generic
slot="error", which you fill yourself from acui-field-invalidlistener — perfect for plugging into i18next, FormatJS, or any translation function you already have. -
Browser fallback — when nothing is slotted, the field renders
control.validationMessage, which the browser already localizes to the user's UA locale.
Pre-localized strings. No JavaScript wiring required. Best when your i18n framework renders strings at build or server-render time.
A single slot="error" + an event listener. Plug in any translation function.
Switching locale calls field.validate() to refresh the visible message immediately.
No slots, no JavaScript. The browser's localized validationMessage is rendered into
the field's internal error region. The cheapest path to an accessible form.
Async & server errors
Custom validators may return a Promise<string | null>. They run after native
validity passes — cheap constraints fail fast and you only pay for an HTTP round-trip when the
input is otherwise valid. Server errors returned from the submit handler land via
field.setError(message).
Cross-field rules
Cross-field validation lives on the dependent field. The validator reads the other control's value
when it runs, no extra bookkeeping required. Re-validate the dependent field from an
input listener on its peer so it doesn't stay stale.
Tokens
Per-instance overrides via CSS custom properties.
| Variable | Default |
|---|---|
--cui-input-bg |
--cui-color-surface-raised |
--cui-input-border |
--cui-color-border |
--cui-input-color |
--cui-color |
--cui-input-placeholder |
--cui-color-muted |
--cui-input-radius |
--cui-control-radius |
--cui-input-padding-block |
0.625rem |
--cui-input-padding-inline |
0.875rem |
--cui-switch-bg |
--cui-color-surface-muted |
--cui-switch-bg-on |
--cui-color-accent |
--cui-switch-knob |
--cui-color-surface-raised |
See Theming → Component tokens for the underlying
--cui-color-* palette.