Skip to content
Dethink Components

ComponentsFormField

FormField

Connect a form control to its label, help text, and error message.

On this page

Use this component from the local workspace package. Follow the setup guide first. A public npm package and registry are not available yet.

View registry files

Import the component into your page or component file.

Usage
import {
  Field,
  FieldControl,
  FieldDescription,
  FieldError,
  FieldLabel,
  Form,
} from "@dethink/components";

Try the examples, then open the code to use them in your app.

Label, description, and error

FieldControl injects the wiring into the wrapped control — the description is announced with the field, and the error replaces it when invalid.

Lowercase letters, numbers, and hyphens only.

This key was revoked on June 2.
Show sourceexamples/form-field/basic.tsx
examples/form-field/basic.tsx
"use client";

import {
  Field,
  FieldControl,
  FieldDescription,
  FieldError,
  FieldLabel,
  Input,
} from "@dethink/components";

export function FormFieldBasic() {
  return (
    <div className="mx-auto grid max-w-md gap-6">
      <Field id="ff-handle">
        <FieldLabel>Handle</FieldLabel>
        <FieldControl asChild>
          <Input placeholder="acme-corp" />
        </FieldControl>
        <FieldDescription>
          Lowercase letters, numbers, and hyphens only.
        </FieldDescription>
      </Field>
      <Field id="ff-api-key" invalid>
        <FieldLabel>API key</FieldLabel>
        <FieldControl asChild>
          <Input defaultValue="sk_live_…" />
        </FieldControl>
        <FieldError>This key was revoked on June 2.</FieldError>
      </Field>
    </div>
  );
}

Fieldset groups

FieldSet and FieldLegend name a related set; horizontal fields put the control before its label — the checkbox layout.

Notification channels
Show sourceexamples/form-field/group.tsx
examples/form-field/group.tsx
"use client";

import {
  Checkbox,
  Field,
  FieldControl,
  FieldGroup,
  FieldLabel,
  FieldLegend,
  FieldSet,
} from "@dethink/components";

export function FormFieldGroup() {
  return (
    <div className="mx-auto max-w-sm">
      <FieldSet>
        <FieldLegend>Notification channels</FieldLegend>
        <FieldGroup>
          <Field id="ffg-email" orientation="horizontal">
            <FieldControl asChild>
              <Checkbox name="channels" value="email" defaultChecked />
            </FieldControl>
            <FieldLabel>Email</FieldLabel>
          </Field>
          <Field id="ffg-slack" orientation="horizontal">
            <FieldControl asChild>
              <Checkbox name="channels" value="slack" />
            </FieldControl>
            <FieldLabel>Slack</FieldLabel>
          </Field>
          <Field id="ffg-sms" orientation="horizontal" disabled>
            <FieldControl asChild>
              <Checkbox name="channels" value="sms" />
            </FieldControl>
            <FieldLabel>SMS (requires verified phone)</FieldLabel>
          </Field>
        </FieldGroup>
      </FieldSet>
    </div>
  );
}

Examples that combine components for common tasks.

Create-project form

One anatomy across four control types — Input, Select, Textarea, and Switch — with on-submit validation rendered through FieldError so each error is announced with its field.

Region

Optional, shown on the project card.

Show sourceexamples/form-field/recipe-project-form.tsx
examples/form-field/recipe-project-form.tsx
"use client";

import { useState } from "react";
import {
  Button,
  Field,
  FieldControl,
  FieldDescription,
  FieldError,
  FieldLabel,
  Form,
  Input,
  Select,
  SelectItem,
  Switch,
  Textarea,
} from "@dethink/components";

type Errors = Partial<Record<"name" | "region", string>>;

/**
 * One Field wrapper per control — Input, Select, Textarea, Switch — shows the
 * same anatomy carrying every control type. Validation runs on submit and
 * errors render through FieldError so they are announced with the field.
 */
export function FormFieldRecipeProjectForm() {
  const [errors, setErrors] = useState<Errors>({});
  const [created, setCreated] = useState(false);

  return (
    <Form
      className="mx-auto max-w-sm space-y-5"
      onSubmit={(event) => {
        event.preventDefault();
        const data = new FormData(event.currentTarget);
        const nextErrors: Errors = {};
        if (!String(data.get("project-name") ?? "").trim()) {
          nextErrors.name = "Give the project a name.";
        }
        if (!data.get("project-region")) {
          nextErrors.region = "Pick a region before creating.";
        }
        setErrors(nextErrors);
        setCreated(Object.keys(nextErrors).length === 0);
      }}
    >
      <Field id="proj-name" invalid={Boolean(errors.name)} required>
        <FieldLabel>Project name</FieldLabel>
        <FieldControl asChild>
          <Input name="project-name" placeholder="apollo" />
        </FieldControl>
        {errors.name ? <FieldError>{errors.name}</FieldError> : null}
      </Field>

      <Select
        label="Region"
        name="project-region"
        placeholder="Choose a region"
        invalid={Boolean(errors.region)}
        errorMessage={errors.region}
        required
      >
        <SelectItem value="us-east">US East</SelectItem>
        <SelectItem value="eu-west">EU West</SelectItem>
      </Select>

      <Field id="proj-desc">
        <FieldLabel>Description</FieldLabel>
        <FieldControl asChild>
          <Textarea name="project-description" rows={2} />
        </FieldControl>
        <FieldDescription>
          Optional, shown on the project card.
        </FieldDescription>
      </Field>

      <Field id="proj-public" orientation="horizontal">
        <FieldControl asChild>
          <Switch name="project-public" />
        </FieldControl>
        <FieldLabel>Public project</FieldLabel>
      </Field>

      <div className="flex items-center justify-between">
        <p aria-live="polite" className="text-muted-foreground text-sm">
          {created ? "Project created ✓" : ""}
        </p>
        <Button type="submit">Create project</Button>
      </div>
    </Form>
  );
}

Field carries the shared state; the part components read it through context.

Field props
PropWhat it doesDefault
idstringBase id that wires the control, label, description, and error together.generated
orientation"vertical" | "horizontal"Vertical stacks label over control; horizontal puts the control first — the checkbox/switch layout."vertical"
required / disabled / readOnly / invalidbooleanField-level states forwarded to the control and reflected in labels and messages.false
aselementSemantic element the field renders as."div"
Field parts
PropWhat it doesDefault
FieldControlasChild + slot propsWraps the actual control and injects id, aria-describedby, aria-errormessage, and state attributes.Not set
FieldLabellabel propsLabel associated with the control; click focuses/toggles it.Not set
FieldDescriptiontext propsMuted helper text announced via aria-describedby.Not set
FieldErrorchildren | errors: FieldErrorItem[]Validation message announced via aria-errormessage; accepts a list of error items.Not set
FieldContentcontainer propsGroups label and description beside a horizontal control (checkbox, switch).Not set
FieldSet / FieldLegend / FieldGroupfieldset anatomyNative fieldset semantics for related fields — the wrapper for radio groups and checkbox sets.Not set
Formform propsStyled form wrapper with consistent vertical rhythm.Not set