Liking cljdoc? Tell your friends :D

x-form-field

A self-contained, form-associated text-input field with a label, optional hint, and optional error message. The component owns its internal <input> element inside shadow DOM. Consumers control validity by setting the error attribute.


Tag name

<x-form-field></x-form-field>

Observed attributes

AttributeTypeDefaultNotes
labelstring""Visible label text (hidden when empty)
typeenum"text"One of: text, email, password, url, number, tel
namestring""Form field name (passed to ElementInternals)
valuestring""Current input value
placeholderstring""Input placeholder
hintstring""Helper text below input (hidden when empty)
errorstring""Error message (hidden when empty; marks invalid)
disabledbooleanfalseDisables the field
readonlybooleanfalseMakes the field read-only
requiredbooleanfalseMarks required (native + ARIA)
autocompletestring""Passed through to inner <input>

Properties

PropertyTypeReflects attribute
valuestringvalue
disabledbooleandisabled
readOnlybooleanreadonly
requiredbooleanrequired
namestringname
placeholderstringplaceholder
autocompletestringautocomplete

Value sync note

The value property setter updates both the value attribute and the shadow <input> value. On user input, the component does not reflect back to the value attribute (avoids cursor jumps). The attribute only changes when set programmatically.


Events

EventBubblesComposedCancelableDetail
x-form-field-inputtruetruefalse{ name: string, value: string }
x-form-field-changetruetruefalse{ name: string, value: string }

x-form-field-input fires on every keystroke (mirrors native input event). x-form-field-change fires on blur/commit (mirrors native change event).


Shadow DOM structure

<style>…</style>
<div part="field">
  <label part="label" id="label" for="input">…</label>
  <div part="input-wrapper">
    <input part="input" id="input"
           aria-labelledby="label"
           aria-describedby="hint error" />
  </div>
  <span part="hint"  id="hint"  aria-live="polite">…</span>
  <span part="error" id="error" role="alert" aria-live="assertive">…</span>
</div>

aria-describedby is set conditionally: includes only present ids (hint, error, or both).


CSS custom properties

PropertyDefault (light)
--x-form-field-label-color#374151
--x-form-field-label-font-size0.875rem
--x-form-field-input-bg#ffffff
--x-form-field-input-color#111827
--x-form-field-input-border1px solid #d1d5db
--x-form-field-input-border-radius6px
--x-form-field-input-padding0.5rem 0.75rem
--x-form-field-focus-ring-color#2563eb
--x-form-field-error-color#dc2626
--x-form-field-hint-color#6b7280
--x-form-field-disabled-opacity0.45

Dark-mode defaults are applied automatically via @media (prefers-color-scheme: dark).


Form association

x-form-field sets formAssociated = true. It participates in <form> submit via ElementInternals.setFormValue(). The field name comes from the name attribute.

CallbackBehaviour
formDisabledCallback(d)Sets/removes disabled attribute, re-renders
formResetCallback()Clears input value and value attribute, calls setFormValue("")

Validity is set via setValidity:

  • error attribute non-empty → { customError: true } with the error message
  • error attribute empty/absent → {} (valid)

Accessibility

  • Inner <input> is labelled via aria-labelledby="label".
  • aria-describedby links to hint and/or error spans when present.
  • aria-invalid="true" is set on the input when an error is present.
  • aria-required mirrors the required attribute.
  • Error span has role="alert" and aria-live="assertive".
  • Hint span has aria-live="polite".
  • Animations respect @media (prefers-reduced-motion: reduce).

Usage examples

Basic

<x-form-field
  label="Email address"
  type="email"
  name="email"
  placeholder="you@example.com"
  hint="We will never share your email.">
</x-form-field>

With error

<x-form-field
  label="Password"
  type="password"
  name="password"
  error="Password must be at least 8 characters.">
</x-form-field>

Listening to events

const field = document.querySelector('x-form-field');

field.addEventListener('x-form-field-input', e => {
  console.log('typing:', e.detail.name, e.detail.value);
});

field.addEventListener('x-form-field-change', e => {
  console.log('committed:', e.detail.name, e.detail.value);
});

Setting value programmatically

// Via property (also syncs the shadow input)
field.value = 'hello@example.com';

// Via attribute
field.setAttribute('value', 'hello@example.com');

Setting error programmatically

// Show error
field.setAttribute('error', 'Invalid email address.');

// Clear error
field.removeAttribute('error');

Inside a form

<form id="my-form">
  <x-form-field label="Name" name="name"></x-form-field>
  <button type="submit">Submit</button>
</form>

<script>
  document.getElementById('my-form').addEventListener('submit', e => {
    e.preventDefault();
    const data = Object.fromEntries(new FormData(e.target));
    console.log(data); // { name: "…" }
  });
</script>

CSS theming

x-form-field {
  --x-form-field-input-border-radius: 0;
  --x-form-field-focus-ring-color: #7c3aed;
  --x-form-field-error-color: #b91c1c;
}

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close