Field

Combine labels, controls, and help text to compose accessible form fields and grouped inputs.

Usage

Field provides the shell and the accessibility plumbing: FieldLabel, Input, FieldDescription, and FieldError connect automatically (htmlFor / id / aria-describedby), so there are no manual ids.

Adornments are documented on InputGroup. For every combination and state side by side, see the Field Matrix.

import {
  Field,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
  FieldLegend,
  FieldSet,
} from '@epilot/spark-ui/field'
import { Input } from '@epilot/spark-ui/input'
<Field>
  <FieldLabel>Device name</FieldLabel>
  <Input placeholder="Garage wallbox" />
  <FieldDescription>Shown on the dashboard.</FieldDescription>
</Field>

Examples

With a hint

We'll never share your email.

Invalid with error message

FieldError renders a role="alert" message. Pass the message as children:

<FieldError>Enter a valid email address.</FieldError>

Use the errors prop when you already hold a form library's error array (react-hook-form's fieldState.errors, TanStack Form's field.state.meta.errors) — it dedupes by message and renders more than one as a list:

<FieldError errors={fieldState.errors} />

Don't hand-write the [{ message }] shape; that's what children are for. It renders nothing when there are no errors.

Keep one FieldError per Field. Several messages belong in one element, via errors or your own markup in children — sibling FieldErrors would each claim the same id and only the first would be announced.

While it has content it links to the control via aria-describedby, listed before the description, so tabbing back into the field announces the error first, and it marks the Field data-invalid plus the control aria-invalid. Pass data-invalid yourself to override that. Passing your own id opts out of the wiring entirely — the control then gets no aria-describedby link at all.

Read-only value

FieldValue shows a label–value pair in the field shell (replaces the old DisplayField). Pass readOnly on Field so the row is named via aria-labelledby instead of an invalid <label htmlFor> on the value div. Long values wrap by default; pass truncate for a single-line ellipsis. String values then get an automatic title so the full value is reachable on hover.

Serial number
WB-2041-8845

Loading skeleton

FieldSkeleton composes label and value placeholders inside the Field shell. Match ECP's gray-soft read-only look with inputColor="gray" and inputStyle="soft". Pass aria-label on Field so the loading row has an accessible name.

With an action: lay the Field out as a row. FieldContent stacks the label and value, the action sits beside it (label it in the app's language).

Account email
you@email.com

Field group

FieldGroup stacks related fields with consistent spacing.

Field set

FieldSet + FieldLegend group fields under a semantic <fieldset> / <legend>.

Address

Complete form

Charging schedule
Shown on the dashboard.

API Reference

Field

All native <div> props, plus:

PropTypeDefault
labelPosition"outset" | "inset"follows the data-label-position knob (inset)
inputStyle"soft" | "surface" | "outlined" | "ghost"follows the data-input-style knob (soft)
inputColor"brand" | "gray"follows the data-input-color knob (brand)
readOnlybooleanfalse — set for FieldLabel + FieldValue rows

Each prop follows its theming knob, resolved through the <Theme> provider (<Theme labelPosition inputStyle inputColor>); the prop overrides per instance. Raw data-* attributes without the provider don't style Field/Input.

The box carrier follows the position: inset → the Field shell (its input keeps the classic accent underline), otherwise the Input or InputGroup.

Old API mapping: variant="underline" → default (inset + soft), variant="outline"outset + outlined. The floating-label look (variant="floating" / labelPosition="overlap") was removed; use inset.

State attributes (pair with the matching prop on the input):

AttributePair withEffect
data-invalidaria-invalid on the inputset automatically by a rendered FieldError; label + text turn error red, filled inset shells tint error-soft
data-disableddisabled on the inputcontrol text dims, filled inset shells tint gray-soft; the label keeps full contrast

Subcomponents

ComponentRendersNotes
FieldLabel<label> or <div>htmlFor wired to the input; renders a <div id={labelId}> when readOnly
FieldDescription<div>linked to the input via aria-describedby automatically
FieldError<div role="alert">children, or errors?: Array<{ message?: string }> deduped; hidden if empty; links aria-describedby and marks the field invalid
FieldValue<div>read-only value; wraps by default, truncate for ellipsis + auto title
FieldSkeleton<div aria-hidden>loading placeholder for read-only rows; label + value bars via Skeleton
FieldSet<fieldset>groups related fields
FieldLegend<legend>variant: "legend" (default) | "label"
FieldGroup<div>vertical stack of fields
FieldContent<div>flex column for label + description next to a control
FieldTitle<div>label-styled title for non-label contexts
FieldSeparator<div> + Separatoroptional inline content (e.g. "or")

On this page