Add this component

npx zentauri-ui add field

Accessibility notes

Keyboard accessible by default with semantic markup, visible focus treatment, and tokenized states. Add descriptive labels for icon-only or decorative usage.

Dependency notes

Check Installation for shared peers. This component keeps styling in Tailwind classes and the --zui-* token contract.

Forms

Form / Field for accessible input layouts

Compose existing inputs, textareas, checkboxes, and selects with connected labels, descriptions, and error messages. Keep validation and submission in your application while Field handles consistent layout and accessibility attributes.

We will only use this for account updates.

Form / Field playground

Explore labels, instructions, required markers, disabled controls, errors, and responsive orientations.

Responsive orientation stacks the label above the control below 640px. Horizontal keeps both side by side. Field size changes spacing and text; Input size is configured separately.


A short name makes your workspace easier to find.

Appearance gallery

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Your project home.

Form and field recipes

A connected label and description


We will only use this for account updates.

Validation with focus and a submission result


Try submitting an empty or invalid address.

Optional context for the team.

Responsive fields with Grid


Profile details

Fields stack on small screens, with two columns on larger screens.

Grouped checkbox choices


Notification channels

Choose where you receive product updates.

A custom Select with a shared trigger ID


A custom Select uses the Field control ID on its root.

Composition and accessibility

Field owns a single control’s label, description, and error through its label, description, and error props. Connections exist in server markup and update with the content. Its generated control ID is independent of the wrapper id; use controlId when you need an explicit ID. Each ID must be unique in the document.

FieldControl accepts exactly one element that forwards attributes to its focusable control. It assigns the Field ID, combines description IDs, and retains the child’s ref, event handlers, value, and native attributes. A field label names the control; an explicit aria-label or aria-labelledby on the child takes precedence. Native disabled/required on the child are retained. For custom composition use the render function or useFieldControl; merge any existing aria-describedby when spreading attributes yourself.

Use FieldControl with Input without Input’s own label or errorMessage to avoid duplicated labels and messages. Native inputs, textareas, selects, and Checkbox work with the default native mode. For Select, share the Field ID with Select’s triggerId, wrap SelectTrigger with FieldControl native=false, and provide role=combobox. Other custom widgets must implement their own disabled and required behavior; aria-required does not validate or block submission. A hidden input can carry a custom selection into FormData.

Form preserves native form props and browser validation. Field error implies invalid, with an explicit invalid override available. Field never runs validation, submits data, or focuses controls automatically. The validation recipe uses noValidate intentionally, reads native validity, and focuses the failed control. Keep visible error messages clear and connect them to their controls. Render errors after validation. Inline Field errors are passive descriptions, read with the control when focused. Standalone FieldError supports an explicit role=alert for consumer-owned notifications; avoid announcing the same message through both a live region and a focused description.

FieldGroup renders a native fieldset with an optional legend and connected description. Its disabled prop uses browser semantics, including the first-legend exception. FieldLegend, FieldLabel, FieldDescription, and FieldError can be used independently with caller-owned IDs. When composing these primitives manually, explicitly wire htmlFor and aria-describedby; only Field’s description/error props are connected automatically.

Vertical is the default. Responsive places the label beside the control from 640px and stacks below it; horizontal keeps columns at all widths. Grid can arrange multiple Field components and stacks them on mobile by default. Form and Field add no validation or animation dependencies.

WAI guidance on labels, grouping, and error notifications

Field API

Generated from the package prop types and variant definitions.

Field

FieldProps

Variants

PropTypeDefault
appearance
bluecontrastcyandefaultemeraldglassgradient-bluegradient-greengradient-orangegradient-pinkgradient-purplegray+14
default
orientation
horizontalresponsivevertical
vertical
size
lgmdsm
md

Behavior

PropTypeDefault
controlIdID for the focusable control. Overrides the child's id through FieldControl; wrapper id stays independent.string | undefinednone
descriptionInstructions automatically connected through aria-describedby.ReactNodenone
disabledApply native disabled to FieldControl.boolean | undefinedfalse
errorVisible error connected through aria-describedby; implies invalid unless invalid is explicitly false.ReactNodenone
invalidOverride the state inferred from error. Does not run validation.boolean | undefinednone
labelVisible label connected to the control. Provide aria-label on the control if omitted.ReactNodenone
requiredApply native required to FieldControl and show a decorative marker.boolean | undefinedfalse

Content

PropTypeDefault
childrenA FieldControl child, or a render function receiving attributes for your own control.ReactNode | ((control: FieldControlAttributes) => ReactNode)none
Inherited HTML props
PropTypeDefault
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLDivElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone

FieldControl

FieldControlProps

Behavior

PropTypeDefault
nativeFalse uses aria-required instead of native required for a custom widget. Widget behavior is owned by the consumer.boolean | undefinedtrue

Content

PropTypeDefault
children*Exactly one element that forwards id, aria attributes, disabled and required to its focusable control.ReactElement<unknown, string | JSXElementConstructor<any>>none

FieldDescription

FieldDescriptionProps

Inherited HTML props
PropTypeDefault
childrenReactNodenone
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLParagraphElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone

FieldError

FieldErrorProps

Inherited HTML props
PropTypeDefault
childrenReactNodenone
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLParagraphElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone

FieldGroup

FieldGroupProps

Behavior

PropTypeDefault
descriptionInstructions describing the group, connected to the fieldset.ReactNodenone
legendNative legend naming this group. Omit when composing FieldLegend manually.ReactNodenone
Inherited HTML props
PropTypeDefault
childrenReactNodenone
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLFieldSetElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone

FieldLabel

FieldLabelProps

Behavior

PropTypeDefault
requiredboolean | undefinednone
Inherited HTML props
PropTypeDefault
childrenReactNodenone
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLLabelElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone

FieldLegend

FieldLegendProps

Inherited HTML props
PropTypeDefault
childrenReactNodenone
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLLegendElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone

Form

FormProps

Inherited HTML props
PropTypeDefault
childrenReactNodenone
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLFormElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone

Installation and theme overrides

pnpm add @zentauri-ui/zentauri-components
# Or vendor source, including the Input used in these recipes:
pnpm dlx @zentauri-ui/zentauri-components add field inputs

Add the package to Tailwind v4 scanning and adjust the source path for your stylesheet location. Vendored files also need to be in scan scope.

@import "tailwindcss";
@source "../node_modules/@zentauri-ui/zentauri-components";

.profile-form {
  --zui-field-form-gap: 2rem;
  --zui-field-form-gap-dark: 2rem;
  --zui-field-radius: 1rem;
  --zui-field-radius-dark: 1rem;
  --zui-field-label-width: 8rem;
  --zui-field-label-width-dark: 8rem;
}

Apply className="profile-form" to Form or a surrounding container. Field controls use their own Input, Checkbox, or Select tokens. Field sizes change label typography and spacing independently of the control size.

Browse all Field light and dark tokens
CSS variable overrides

Form / Field CSS variables

Theme field surfaces, labels, instructions, error indicators, and form/group spacing. Each token has a paired dark override; control styling remains owned by Input, Checkbox, or Select.

226 variables

Pattern: --zui-<component>-<slot?>-<variant?>-<property>-<state?>-dark?

:root {
  --zui-field-radius: 0.75rem;
  --zui-field-border-width: 1px;
  --zui-field-padding: 0.75rem;
  --zui-field-invalid-border: #e11d48;
  --zui-field-disabled-opacity: 0.5;
  --zui-field-label-width: 10rem;
  --zui-field-gap-sm: 0.375rem;
  --zui-field-font-size-sm: 0.75rem;
  --zui-field-gap-md: 0.5rem;
  --zui-field-font-size-md: 0.875rem;
  --zui-field-gap-lg: 0.75rem;
  --zui-field-font-size-lg: 1rem;
  --zui-field-content-gap: 0.5rem;
  --zui-field-label-gap: 0.25rem;
  --zui-field-label-weight: 500;
  --zui-field-label-fg: inherit;
  --zui-field-description-font-size: 0.8125rem;
  --zui-field-description-fg: inherit;
  --zui-field-description-opacity: 0.9;
  --zui-field-error-gap: 0.375rem;
  --zui-field-error-font-size: 0.8125rem;
  --zui-field-error-fg: inherit;
  --zui-field-error-icon-size: 1rem;
  --zui-field-form-gap: 1.5rem;
  --zui-field-group-gap: 1rem;
  --zui-field-legend-gap: 0.5rem;
  --zui-field-legend-font-size: 1rem;
  --zui-field-legend-weight: 600;
  --zui-field-legend-fg: inherit;
  --zui-field-default-bg: transparent;
  --zui-field-default-fg: #0f172a;
  --zui-field-default-border: transparent;
  --zui-field-subtle-bg: #f8fafc;
  --zui-field-subtle-fg: #334155;
  --zui-field-subtle-border: #e2e8f0;
  --zui-field-contrast-bg: #0f172a;
  --zui-field-contrast-fg: #f8fafc;
  --zui-field-contrast-border: #334155;
  --zui-field-glass-bg: #ffffffb3;
  --zui-field-glass-fg: #0f172a;
  --zui-field-glass-border: #cbd5e1;
  --zui-field-glass-blur: 12px;
  --zui-field-blue-bg: #eff6ff;
  --zui-field-blue-fg: #1e40af;
  --zui-field-blue-border: #bfdbfe;
  --zui-field-cyan-bg: #ecfeff;
  --zui-field-cyan-fg: #155e75;
  --zui-field-cyan-border: #a5f3fc;
  --zui-field-green-bg: #f0fdf4;
  --zui-field-green-fg: #166534;
  --zui-field-green-border: #bbf7d0;
  --zui-field-lime-bg: #f7fee7;
  --zui-field-lime-fg: #3f6212;
  --zui-field-lime-border: #d9f99d;
  --zui-field-emerald-bg: #ecfdf5;
  --zui-field-emerald-fg: #065f46;
  --zui-field-emerald-border: #a7f3d0;
  --zui-field-indigo-bg: #eef2ff;
  --zui-field-indigo-fg: #3730a3;
  --zui-field-indigo-border: #c7d2fe;
  --zui-field-purple-bg: #faf5ff;
  --zui-field-purple-fg: #6b21a8;
  --zui-field-purple-border: #e9d5ff;
  --zui-field-pink-bg: #fdf2f8;
  --zui-field-pink-fg: #9d174d;
  --zui-field-pink-border: #fbcfe8;
  --zui-field-rose-bg: #fff1f2;
  --zui-field-rose-fg: #9f1239;
  --zui-field-rose-border: #fecdd3;
  --zui-field-sky-bg: #f0f9ff;
  --zui-field-sky-fg: #075985;
  --zui-field-sky-border: #bae6fd;
  --zui-field-teal-bg: #f0fdfa;
  --zui-field-teal-fg: #115e59;
  --zui-field-teal-border: #99f6e4;
  --zui-field-yellow-bg: #fefce8;
  --zui-field-yellow-fg: #854d0e;
  --zui-field-yellow-border: #fef08a;
  --zui-field-orange-bg: #fff7ed;
  --zui-field-orange-fg: #9a3412;
  --zui-field-orange-border: #fed7aa;
  --zui-field-red-bg: #fef2f2;
  --zui-field-red-fg: #991b1b;
  --zui-field-red-border: #fecaca;
  --zui-field-slate-bg: #f8fafc;
  --zui-field-slate-fg: #334155;
  --zui-field-slate-border: #cbd5e1;
  --zui-field-gray-bg: #f9fafb;
  --zui-field-gray-fg: #374151;
  --zui-field-gray-border: #d1d5db;
  --zui-field-zinc-bg: #fafafa;
  --zui-field-zinc-fg: #3f3f46;
  --zui-field-zinc-border: #d4d4d8;
  --zui-field-gradient-blue-from: #eff6ff;
  --zui-field-gradient-blue-to: #faf5ff;
  --zui-field-gradient-blue-fg: #0f172a;
  --zui-field-gradient-blue-border: #bfdbfe;
  --zui-field-gradient-green-from: #f0fdf4;
  --zui-field-gradient-green-to: #f7fee7;
  --zui-field-gradient-green-fg: #0f172a;
  --zui-field-gradient-green-border: #bbf7d0;
  --zui-field-gradient-purple-from: #faf5ff;
  --zui-field-gradient-purple-to: #fdf2f8;
  --zui-field-gradient-purple-fg: #0f172a;
  --zui-field-gradient-purple-border: #e9d5ff;
  --zui-field-gradient-orange-from: #fff7ed;
  --zui-field-gradient-orange-to: #fefce8;
  --zui-field-gradient-orange-fg: #0f172a;
  --zui-field-gradient-orange-border: #fed7aa;
  --zui-field-gradient-pink-from: #fdf2f8;
  --zui-field-gradient-pink-to: #fff1f2;
  --zui-field-gradient-pink-fg: #0f172a;
  --zui-field-gradient-pink-border: #fbcfe8;
}

/* Dark theme variables follow the same names with -dark appended. */
.dark {
  --zui-field-radius-dark: 0.75rem;
  --zui-field-border-width-dark: 1px;
  /* ...same variables with -dark at the end */
}

What it does

Consistent form and field layouts with native semantics, stable accessibility connections, 26 appearances, and paired light/dark theme tokens.

Common use cases

  • Account and profile forms.
  • Settings with grouped checkbox choices.
  • Responsive multi-column forms using Grid.
  • Custom controls with explicit accessibility connections.

Accessibility

Labels identify controls, description/error IDs are connected with aria-describedby, and native fieldset/legend semantics name groups. Required markers are decorative; native required and aria-required convey the state. Apps own validation and focus management.

FAQ

Does Field manage form state or validation?

No. Form is a native form wrapper. Field presents labels, instructions, and errors; you own values, validation, submission, and focus. It requires no form-state library.

How do labels and errors connect to the control?

Pass label, description, and error to Field, then wrap one control in FieldControl. IDs and aria-describedby are created during rendering, including on the server. Use controlId to provide a unique explicit ID.

Can I use custom controls?

Yes. FieldControl needs a child that forwards ID and ARIA props to its focusable element. Alternatively use the Field render function or useFieldControl. For Select, set triggerId to the Field ID and wrap SelectTrigger with native=false and role=combobox.

How do required and disabled work?

FieldControl applies native required and disabled by default. Custom mode uses aria-required; the custom widget must implement validation itself. FieldGroup uses native disabled fieldset semantics.

How do fields respond to mobile widths?

Fields stack vertically by default. Responsive orientation adds label/control columns at 640px. Horizontal forces columns at every width. Grid arranges multiple fields and stacks them on mobile by default.

Can I use the individual primitives?

Yes. Use FieldLabel, FieldDescription, FieldError, FieldGroup, and FieldLegend independently. Supply and connect IDs yourself; automatic descriptions and errors come from the Field props.