Skip to content
Dethink Components

ComponentsFormField

FormField

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

On this page

Install component source from the public registry, or use the local workspace package. Follow the setup guide first. The npm package is not published yet.

View registry files

Implemented with React and browser APIs, without an additional behavior or rendering library.

Shared foundation: React, clsx for conditional classes, tailwind-merge for resolving utility-class conflicts. Styles use Tailwind CSS and Dethink CSS variables.

Based on this component’s source and registry composition. Libraries used only in showcase examples are excluded.

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