Skip to content

Components · Field

Field

Field is where LUC decides label semantics: it generates the control's id and wires the description, the pending check and the message to it. The rest of the page is the parts a form is assembled from — one field shell with addons, a standalone label for third-party controls, the platform's own picker, and the keyboard hints that make a shortcut discoverable.

import { Field, FieldGroup, Fieldset, } from "@luc/ui/forms";

Examples

What Field owns — and what a bare Label costs you

Both fields below look alike. Only the left one has a generated id, an `aria-describedby` relationship and `aria-invalid` when the value is rejected; the right one leaves all three to you.

Bars used to warm up indicators.

Bars used to warm up indicators — and the wiring is yours.

A Field already renders the label for a LUC control. Use Label for a third-party or composite widget, where you assemble the ARIA yourself.

Fieldset names a question, FieldGroup is the grid

A `<fieldset>` with a legend is announced with every control inside it, which a heading over a grid is not. `FieldGroup` adds no semantics at all — it is only the columns.

Delivery

Where the daily report is sent.

One recipient per line.

One message slot

Description, pending check, confirmation and error share one row: only the most specific one renders, so a field never stacks four lines of text.

Lowercase, no spaces.

Checking…

Handle is available

One shell, many addons

InputGroup is the same shell every other text control uses, exposed as composition: a prefix, a separator, a suffix, an icon — with the focus ring, the invalid state and the sunk surface already correct.

https://
⌘K

Labels and keycaps

Label exists for the control LUC does not own, and its required asterisk is aria-hidden so the name is not read twice. KbdGroup joins a shortcut into one announced chord.

/Esc⌘K

When native is better

Select is right nine times out of ten. The tenth is a mobile form, where the platform picker is a wheel the user already knows and the custom popup is worse than either.

HoverCard is not a Tooltip

A tooltip labels a control; a HoverCard previews something with content and actions, and follows focus as well as the pointer.

Never the only way to reach the content: the trigger is focusable and the same information belongs on the page it previews.

Usage

import { Field, FieldGroup, Fieldset } from "@luc/ui/forms";
<Field label="Lookback" description="Bars used to warm up indicators.">
  <Input defaultValue="200" />
</Field>

`Field` is the one place the library decides `<label>` semantics: the control inside it inherits the id, the description and the message, so nothing is written by hand.

API

Read from the published TypeScript API, so a prop that is not listed here does not exist and a default that is listed is the one the compiler sees.

Exported symbols
ExportKindSignature
Field13 documented props({ children, id, label, description, error, validating, success, required, disabled, readOnly, labelHint, className, orientation, }: FieldProps) => import("@types/react").JSX.Element
FieldGroup1 documented props({ columns, className, ...props }: FieldGroupProps) => import("@types/react").JSX.Element
Fieldset4 documented props({ legend, description, children, className, disabled, legendHint, ...props }: FieldsetProps) => import("@types/react").JSX.Element
useFieldexport() => { id: string; labelId: string | undefined; describedBy: string | undefined; hasLabel: boolean; invalid: boolean; disabled: boolean; readOnly: boolean; busy: boolean; required: boolean; groupLab…

Field props

Type: FieldProps

Values the compiler reads from the declaration.

Field props
PropTypeDefaultNotes
childrenReactNode—
className?string | undefined—
description?ReactNode—Helper text rendered under the control. Replaced by `error` when present.
disabled?boolean | undefined—
error?ReactNode—Error message. Presence sets `aria-invalid` on the control.
id?string | undefined—Explicit id for the control. Generated when omitted.
label?ReactNode—
labelHint?ReactNode—Optional content rendered next to the label, e.g. a "Why?" hint button.
orientation?"horizontal" | "vertical" | undefined—Compact layouts drop the label row spacing.
readOnly?boolean | undefined—
required?boolean | undefined—
success?ReactNode—Confirmation of a *checked* value (an availability probe, a verified handle). Deliberately not rendered for ordinary "field is filled in" — see DESIGN_SYSTEM.md §12.
validating?ReactNode—Validation in flight. Renders a quiet inline status row and sets `aria-busy` on the control. Pass a node to override "Checking…".
FieldGroup props1 props

FieldGroup props

Type: FieldGroupProps

Values the compiler reads from the declaration.

FieldGroup props
PropTypeDefaultNotes
columns?1 | 2 | 3 | undefined—Column count from the `sm` breakpoint up. A single column on mobile always.
Fieldset props4 props

Fieldset props

Type: FieldsetProps

Values the compiler reads from the declaration.

Fieldset props
PropTypeDefaultNotes
description?ReactNode—
disabled?boolean | undefined—
legendReactNode—The group's accessible name. Always rendered — a fieldset without a legend is a container, not a form group.
legendHint?ReactNode—Optional content in the legend row, e.g. "Select all".

Guidelines

Do

  • Wrap every control in a `Field` and let it own the id, the description and the message rather than writing `htmlFor` by hand.
  • Reach for `Fieldset` with a legend when several controls answer one question, and `FieldGroup` only for the columns.
  • Use `validating` and `success` where they mean something — a check in flight, a value that was actually confirmed.

Don't

  • Stack two messages on one field: description, pending check, success and error share one slot, and the error wins.
  • Read `success` as “the reader filled this in” — it is for a checked value, such as an availability probe.
  • Put a `Label` inside a `Field`: the field already renders one, and two labels for one control make the name ambiguous.

Accessibility

The field generates the control's id and hands down the `aria-describedby` list and the invalid, disabled and read-only states through context, so the message the reader sees is the one the control is described by — an error never stacks on top of stale helper text. The error row is a live `role="alert"`, and `required` reaches assistive technology through the control's own attribute while the asterisk stays hidden from it.

Luc UI v1.0Fielddark theme