FormField
Label, helper and error around a control that has none of its own.
Playground
<FormField
error=""
helper="PDF, JPG, PNG — max 10 MB"
label="Document"
>
{#snippet children(ctx)}
<input
id={ctx.id}
type="file"
aria-describedby={ctx.describedBy}
aria-invalid={ctx.invalid || undefined}
required={ctx.required}
class="border-border-subtle w-full rounded-md border px-3 py-2 text-sm"
/>
{/snippet}
</FormField>01 Examples
Reach for FormField around a control that brings no
label, helper or error of its own: a native file or colour input, a widget from somewhere else.
The form blocks here take label, helper and error as props, so a FormField around one of them puts a second
label above the first. The children snippet hands out a FormFieldSlotContext, and each of its fields reaches the
control through a line you write.
Wrapping a native file input
required puts the asterisk on the label and appears in the snippet context. Whether the control enforces anything is down to the attribute you set on it, which is the same division of labour as for the id and the description.<FormField label="Document" required helper="PDF, JPG or PNG, max 10 MB">
{#snippet children({ id, describedBy, invalid, required })}
<input
{id}
type="file"
aria-describedby={describedBy}
aria-invalid={invalid || undefined}
{required}
/>
{/snippet}
</FormField>A native control with an error
error swaps the helper for the message, points describedBy at that message instead, and flips invalid for the control to pick up. invalid is a boolean, so pass it on as invalid || undefined unless you want an aria-invalid=false sitting in the markup.<FormField label="Brand color" error="Pick a color with more contrast">
{#snippet children({ id, describedBy, invalid })}
<input
{id}
type="color"
aria-describedby={describedBy}
aria-invalid={invalid || undefined}
/>
{/snippet}
</FormField>02 Customization
FormField takes class on its outer element and slotClasses keyed by wrapper, label, requiredMark, helper and message, which is the error. It takes no unstyled or preset prop and resolves no provider cascade, so its look
is set at the call site and nowhere else.
requiredMark is the asterisk beside the label. Because
it is a slot, a form where every field is required marks nothing — slotClasses={{ requiredMark: 'hidden' }} here, and — on the fields that do resolve a provider cascade — the same entry under defaults on a BlocksProvider.
See Customization for the class and slotClasses contract shared across blocks.
03 Accessibility
One label, one control
The <label for=…> points at the single id the snippet hands out, so a FormField labels one
control. Two controls in one field want a <fieldset> with a legend instead, and a control
that is not a labelable element wants aria-labelledby pointing at your own label.
Errors are announced
The error message renders with role="alert", so a
message that appears after a failed submit is read out without the user going looking for
it.
04 API Reference
10 propsProp | Type | Default | Description | |
|---|---|---|---|---|
children required | Snippet<[FormFieldSlotContext]> | — | Snippet receiving wiring metadata (id, describedBy, invalid,
required, disabled). The wrapped control should spread or apply
these to itself for accessibility. | |
class | string | — | Extra classes merged onto the wrapper element. | |
disabled | boolean | false | Disables visual emphasis. Pass through to the slot's control as needed. | |
error | string | — | Error message shown below the control. Replaces the helper text and
propagates invalid: true to the slot for ARIA wiring. | |
helper | string | — | Helper text shown below the control. Hidden when error is present.
Named helper to match the built-in helper prop of the form primitives
(Input, Select, Toggle, …) — one vocabulary across the API seam. | |
id | string | — | Explicit HTML id for the control. Auto-generated when omitted, then
forwarded to the slot. Caller may use it to attach external <label for>. | |
label | string | — | Label rendered above the control. Auto-linked to the slot via the generated id. | |
required | boolean | false | Adds a required asterisk to the label and propagates required: true
to the slot. Does **not** apply the native required attribute —
the slot's control is responsible for that. | |
slotClasses | Partial<Record<FormFieldSlots, string>> | — | Per-slot class overrides. Slots: wrapper (what class also targets) |
label | requiredMark | message | helper. | |
...HTMLAttributes<HTMLDivElement> inherited | HTMLAttributes | — | HTML attributes (excluding: 'children') |
05 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
FormFieldProps | interface | props | 0 | — | |
FormFieldSlotContext | interface | helper | 0 | Wiring metadata passed to the FormFieldProps.children snippet. | |
FormFieldSlots | type | variant | 0 | Slot names derived from the tv() config above — single source of truth for slotClasses. | |
SlotNames | type | helper | 0 | Extracts the slot-name union from a slotted tv() config function — the
companion to VariantProps. The slot-mode overload returns
(props?) => { [K in keyof S]: SlotFn }, so keyof ReturnType<T> is exactly
the set of slot names a component declares in tv({ slots: … }).
Use it to type a component's slotClasses prop from the single source of
truth (its *.variants.ts) instead of hand-maintaining a parallel union
that silently drifts when a slot is added or renamed: |
06 Installation
Import
import { FormField } from '@urbicon-ui/blocks';