videorc/ui

Field

Labels, descriptions and errors for a form control, plus Videorc's SettingsRow and the grouped settings list. A Field names and describes its control with no ids to wire.

Stream details

YouTube asks every stream to say whether it's made for kids.

Installation

$ pnpm dlx shadcn@latest add @videorc/field

This brings field-context, the module every form control reads to find its Field.

Usage

import {
  Field,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
  SettingsRow,
} from "@/components/ui/field";
<Field>
  <FieldLabel>Stream title</FieldLabel>
  <Input placeholder="Untitled livestream" />
  <FieldDescription>Shown on YouTube and Twitch.</FieldDescription>
</Field>

Anatomy

<FieldSet>
  <FieldLegend />
  <FieldGroup>
    <Field>
      <FieldLabel />
      {/* the control */}
      <FieldDescription />
      <FieldError />
    </Field>
    <FieldSeparator />
    <Field orientation="horizontal">
      <FieldContent>
        <FieldLabel />
        <FieldDescription />
      </FieldContent>
      {/* the control */}
    </Field>
  </FieldGroup>
</FieldSet>

<FieldGroup variant="grouped">
  <SettingsRow label="…" description="…" error="…">
    {/* the control */}
  </SettingsRow>
</FieldGroup>

Examples

Settings rows

A settings page is a FieldGroup variant="grouped" of SettingsRows: one inset group split by hairlines, each row with its label and 12 px description on the left and its control on the right. error puts the row's own refusal or failure under the description. A row whose control is a button names its group with aria-label, since a label can't name a button.

~/Movies/Videorc

Keep a copy on this computer while you stream.

Show chat under Session while you're on air.

Choice cards

Wrap a horizontal Field in a FieldLabel to make the whole card the checkbox's label: a click anywhere toggles it, and the card shows the focus ring. data-slot="checkbox-group" on the FieldGroup tightens the gap.

Theming

Token or attributeMeaning
rounded-row, bg-inset, divide-borderThe grouped variant: one inset group split by hairlines
text-meta, text-muted-foregroundDescriptions, at 12 px in the secondary tier
text-foreground, text-destructiveAn error: monochrome words, the error glyph in the destructive tone
data-orientationvertical, horizontal or responsive on a Field
data-variantdefault or grouped on a FieldGroup
data-settings-rowMarks a SettingsRow's Field
data-disabled="true"On a Field, dims its label

Accessibility

Naming and describing

A Field names its control. Each control and the FieldLabel register an id with the Field when they mount (an explicit id always wins), and then:

  • the label points htmlFor at the first control a label element can name: an Input, a Textarea, a Switch or a Checkbox;
  • a control no label element can point at, such as a Slider thumb or a group of toggles, takes the label through aria-labelledby, unless it has an aria-label of its own;
  • every FieldDescription describes the Field's controls through aria-describedby, unless a control passes its own.

Nothing points at an id that isn't on the page, two controls in one Field never share an id, and outside a Field nothing is added. So a SettingsRow's label names its switch and its description describes it, with no ids at all.

A FieldError is a role="alert", so it is announced when it appears. Mark the control aria-invalid too.

Your own controls

The wiring lives in components/ui/field-context.ts, its own module so a control can read it without pulling the Field's styles into its bundle. A control of your own joins in with the same hooks:

"use client";

import type { ComponentProps } from "react";

import {
  useFieldControlId,
  useFieldDescribedBy,
} from "@/components/ui/field-context";

function ColorWell({
  id,
  "aria-describedby": ariaDescribedBy,
  ...props
}: ComponentProps<"input">) {
  const controlId = useFieldControlId(id);
  const describedBy = useFieldDescribedBy(ariaDescribedBy);
  return (
    <input
      aria-describedby={describedBy}
      id={controlId}
      type="color"
      {...props}
    />
  );
}

API reference

Each part takes every attribute of the element it renders, plus:

FieldGroup

Prop

Type

Field

Prop

Type

FieldLabel

A Label.

Prop

Type

FieldDescription

Prop

Type

FieldError

Prop

Type

FieldLegend

Prop

Type

SettingsRow

Every Field prop except orientation, plus:

Prop

Type

field-context

Prop

Type