GitHub

Field

A form field wrapper that composes Label, input elements, helper text, and error message into a single accessible unit with automatic ID generation.

AccessibleDark ModeAuto-IDError State

Install

$npx react-principles add field
01

Live Demo

Explore all variants and interactive states in Storybook.

Open Storybookopen_in_new

We'll never share your email.

Must be at least 8 characters.

Username is already taken.

expand_more

Tell us about yourself.

This field is disabled.

02

Code Snippet

src/ui/Field.tsx
import { Field } from "@/ui/Field";
import { Input } from "@/ui/Input";

// Basic
<Field label="Email">
  <Input type="email" placeholder="you@example.com" />
</Field>

// With helper text
<Field
  label="Password"
  helperText="Must be at least 8 characters."
  required
>
  <Input type="password" />
</Field>

// With error message
<Field
  label="Username"
  errorMessage="Username is already taken."
>
  <Input placeholder="Choose a username" />
</Field>
03

Copy-Paste (Single File)

Field.tsx
import { cloneElement, forwardRef, type ReactNode, type HTMLAttributes } from "react";
import { Label } from "./Label";
import { cn } from "@/lib/utils";

// ─── Types ────────────────────────────────────────────────────────────────────

export interface FieldProps extends HTMLAttributes<HTMLDivElement> {
  label?: string;
  helperText?: string;
  errorMessage?: string;
  required?: boolean;
  disabled?: boolean;
  id?: string;
  children: ReactNode;
}

// ─── Component ────────────────────────────────────────────────────────────────

export const Field = forwardRef<HTMLDivElement, FieldProps>(function FieldRoot(
  {
    label,
    helperText,
    errorMessage,
    required,
    disabled,
    id,
    className,
    children,
    ...rest
  },
  ref
) {
  // Auto-generate ID from label if not provided
  const fieldId = id ?? (label ? label.toLowerCase().replace(/\s+/g, "-") : undefined);
  const descriptionId = fieldId ? `${fieldId}-description` : undefined;

  // Clone child element and inject accessibility props
  const child = cloneElement(children as React.ReactElement, {
    id: fieldId,
    "aria-describedby": descriptionId,
    "aria-invalid": !!errorMessage,
  } as Record<string, unknown>);

  return (
    <div
      ref={ref}
      className={cn("flex flex-col gap-1.5", disabled && "opacity-50", className)}
      {...rest}
    >
      {label && (
        <Label htmlFor={fieldId} required={required}>
          {label}
        </Label>
      )}

      {child}

      {(helperText || errorMessage) && descriptionId && (
        <p
          id={descriptionId}
          className={cn(
            "text-xs",
            errorMessage
              ? "text-red-500 dark:text-red-400"
              : "text-slate-500 dark:text-slate-400"
          )}
        >
          {errorMessage || helperText}
        </p>
      )}
    </div>
  );
});
04

Props

Extends all native HTMLDivElement attributes.

PropTypeDefaultDescription
labelstringField label text.
helperTextstringDescriptive helper text shown below input.
errorMessagestringError message — replaces helperText when present.
requiredbooleanfalseShows required indicator on label.
disabledbooleanfalseApplies muted opacity style.
idstringauto-generatedID for label-input association.
React Principles