Files
suaveplan/packages/ui/react/react-ui/src/components/field/field.md
2026-05-27 23:47:53 +01:00

6.5 KiB

Field

(no submodules)

- field.variants — `fieldVariants`, `fieldLabelVarian…
  • field.variants — fieldVariants, fieldLabelVariants, fieldDescriptionVariants, and fieldErrorVariants are the four cva (class-var…

  • field.variants — fieldVariants, `fieldLabelVarian…

  • field.variants — fieldVariants, fieldLabelVariants, fieldDescriptionVariants, and fieldErrorVariants are the four cva (class-var…

  • field.variants — fieldVariants, `fieldLabelVarian…

  • field.variants — fieldVariants, fieldLabelVariants, fieldDescriptionVariants, and fieldErrorVariants are the four cva (class-var…

  • field.variants — fieldVariants, `fieldLabelVarian…

  • field.variants — fieldVariants, fieldLabelVariants, fieldDescriptionVariants, and fieldErrorVariants are the four cva (class-var…

  • field.variants — fieldVariants, fieldLabelVariants, fieldDescriptionVariants, and fieldErrorVariants are the four cva (class-var…

Purpose

<Field> is the chassis form-field container — a semantic wrapper around a single form control plus its label, description, and error message. It carries the data-invalid and data-disabled selectors so descendant <FieldLabel> instances pick up the destructive text colour and opacity decay automatically through the cva group-data-[invalid=true]/field: selector.

The four primitives in this module — <Field>, <FieldLabel>, <FieldDescription>, <FieldError> — compose into a complete form-field surface. Per the cross-cutting reconciliation note (#1) shadcn ships field as a separate registry item from field-group; this package mirrors that split so each primitive evolves independently.

<Field> does NOT auto-detect child invalid/disabled state — consumers pass the booleans explicitly. This keeps the chassis a pure styling primitive: the form library (sibling change enterprise-react-forms-v2) owns the validation orchestration and forwards invalid / disabled based on its own state.

Features

  • <Field> container with flex flex-col gap-2 layout (NOT space-y-* per shadcn skill rule).
  • <FieldLabel> built on @radix-ui/react-label so clicking the label focuses the matched input.
  • <FieldDescription> renders a muted <p> for helper text — typically wired to the control via aria-describedby.
  • <FieldError> renders a destructive <p role="alert"> so screen readers announce the message immediately when it appears.
  • data-invalid on <Field> automatically flips descendant <FieldLabel> colour to destructive.
  • data-disabled on <Field> decays opacity.
  • Refs forwarded on every primitive.
  • Theme-driven variant overrides via useThemedVariants("Field" | "FieldLabel" | "FieldDescription" | "FieldError").

Usage: Basic Example

import { Field, FieldLabel, Input } from "@suaveplan/react-ui";

export function NameField() {
    return (
        <Field>
            <FieldLabel htmlFor="name">Name</FieldLabel>
            <Input id="name" />
        </Field>
    );
}

Usage: Advanced Example

import {
    Field,
    FieldDescription,
    FieldError,
    FieldLabel,
    Input,
} from "@suaveplan/react-ui";
import { useState } from "react";

export function PasswordField() {
    const [pwd, setPwd] = useState("");
    const tooShort = pwd.length > 0 && pwd.length < 8;

    return (
        <Field invalid={tooShort}>
            <FieldLabel htmlFor="pwd">Password</FieldLabel>
            <Input
                id="pwd"
                type="password"
                value={pwd}
                invalid={tooShort}
                aria-invalid={tooShort}
                aria-describedby="pwd-help pwd-err"
                onChange={(e) => setPwd(e.currentTarget.value)}
            />
            <FieldDescription id="pwd-help">
                At least 8 characters.
            </FieldDescription>
            {tooShort ? (
                <FieldError id="pwd-err">Too short.</FieldError>
            ) : null}
        </Field>
    );
}

API Reference

<Field>

forwardRef<HTMLDivElement, FieldProps> — renders a <div> container.

FieldProps

Prop Type Default Description
invalid boolean false Drives data-invalid="true" on the container.
disabled boolean false Drives data-disabled="true" on the container.
all native HTMLAttributes<HTMLDivElement> native native Forwarded as-is.

<FieldLabel>

forwardRef<ComponentRef<typeof LabelPrimitive.Root>, FieldLabelProps> — wraps @radix-ui/react-label with the chassis cva applied.

FieldLabelProps

Inherits every prop from LabelPrimitive.Root — htmlFor, children, native HTML attributes.

<FieldDescription>

forwardRef<HTMLParagraphElement, FieldDescriptionProps> — renders a <p> with muted text.

FieldDescriptionProps

Inherits every prop from HTMLAttributes<HTMLParagraphElement>.

<FieldError>

forwardRef<HTMLParagraphElement, FieldErrorProps> — renders a <p> with destructive colour and role="alert" by default.

FieldErrorProps

Inherits every prop from HTMLAttributes<HTMLParagraphElement>. The default role="alert" can be overridden by passing role="status" (for non-blocking errors).

Cva resolvers

fieldVariants, fieldLabelVariants, fieldDescriptionVariants, fieldErrorVariants are all exported as bare cva resolvers for external composition.

Implementation Notes

  • The four primitives are co-located in a single component module because they always compose together; splitting them into four directories would create import-tax for no benefit.
  • <Field> does NOT walk children to detect invalid/disabled state — consumers pass the booleans explicitly. This keeps the chassis a pure styling primitive.
  • The cva group/field named-group selector lets <FieldLabel> flip its colour without needing a sibling <FieldError> to be present.
  • <FieldError> defaults to role="alert" (assertive announcement) so screen readers announce validation errors immediately when they appear. Consumers that want a polite announcement override with role="status".
  • Per the cross-cutting reconciliation note (#1), shadcn ships field and field-group as separate registry items; this chassis mirrors that split so the primitives evolve independently and can be ejected piecemeal.