Skip to content

kit-field

<kit-field label="Email" description="We'll never share it." required>
<input type="email" name="email" />
</kit-field>

<kit-field> provides label + description + error UI for a form control. The control itself is a real native input/textarea/select that you slot in. The field wires up aria-describedby, aria-invalid, the visible required marker, and id/for plumbing.

@atheory-ai/kitsune-ui

import '@atheory-ai/kitsune-ui'
import { KitFieldElement } from '@atheory-ai/kitsune-ui'
AttributeTypeDefaultNotes
labelstringVisible label text.
descriptionstringOptional help text below the control.
errorstringOptional error text. When set, the control gets aria-invalid="true" and aria-describedby points at the error.
requiredbooleanfalseVisual asterisk and required propagated to the slotted control. Reflects.
invalidbooleanderivedReflects when error is non-empty. Style with [invalid].
field-idstringauto-generatedThe id used to wire up label/description/error if the slotted control has no id.
SlotPurpose
(default)The form control: <input>, <textarea>, or <select>.

The control stays in the light DOM so it participates in <form> natively.

<kit-field label="Title" required>
<input name="title" autocomplete="off" />
</kit-field>
<kit-field label="Email" description="We'll send a verification link.">
<input type="email" name="email" />
</kit-field>
<kit-field label="Title" error="Title is required">
<input name="title" />
</kit-field>

The error renders below the control with role="alert" and the input gets aria-invalid="true" and aria-describedby="<id>-error".

<kit-field label="Body">
<textarea name="body" rows="6"></textarea>
</kit-field>
<kit-field label="Tone">
<select name="tone">
<option>Casual</option>
<option>Formal</option>
</select>
</kit-field>

Same wiring works for any standard form control.

PropertyDefault
--kit-space-10.25rem (label↔control gap)
--kit-space-20.5rem (control padding)
--kit-space-30.75rem
--kit-color-surfaceCanvas (control background)
--kit-color-borderCanvasText
--kit-color-textCanvasText
--kit-color-mutedGrayText (description)
--kit-color-erroroklch(58% 0.18 25)
--kit-radius-sm0.375rem
--kit-focus-ringHighlight

The slotted control is a real native control. new FormData(form) reads it. form.requestSubmit() validates it. Constraint validation (required, minlength, pattern, type) all work as the platform defines.

form.addEventListener('submit', (e) => {
e.preventDefault()
const data = Object.fromEntries(new FormData(form).entries())
// ...
})
const field = document.querySelector('kit-field')!
field.error = 'Title is too long'
// later
field.error = ''

When error changes, the field updates the description-binding, sets aria-invalid, and reflects the invalid host attribute for styling.

  • The label is a real <label> with for pointing to the slotted control’s id.
  • If the slotted control has no id, kit-field assigns one automatically.
  • aria-describedby points at the description (when only description is present), or at the error (when error is present), or at both (you can override with aria-describedby on the input).
  • Errors render with role="alert" so screen readers announce them on appearance.
  • Only supports a single slotted control per field.
  • Doesn’t yet expose all native validation messages (e.g., :invalid styling responds, but validationMessage isn’t shown unless you set error manually).

A future iteration will be a fully form-associated custom element with first-class constraint validation. For now, real <input> inside a slot covers the common case.