Skip to content
txbrendan/ui

Field Frame

beta

The frame every field wears: a label above, the control, and a hint or an error under it, wired up for screen readers. Also exports FieldButton, the field-shaped trigger for lists and pickers.

Preview

size

Shirts run small.

Shirts run small.

Shirts run small.

states

Set by your admin.

Saved

Shown on the team page.

18/40

control

Room

Pressing the label focuses the button.

Theme color
Reviewer
Plan

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the component.

    npx shadcn@latest add @txbrendan/field-frame

Use it for

  • Frame any field with it, so every form reads, sizes, and announces the same way.
  • Give the frame the control's id. The label gets <id>-label and the hint or error gets <id>-note.
  • Spread fieldIds(id, { label, hint, error, control }) on the control. It sets aria-describedby, aria-invalid, and, for a button or a group, aria-labelledby. A button cannot take aria-invalid, so it gets data-invalid, which draws the red edge.
  • fieldIds lives in a client file, so call it from a client component. In a server component, write the same ids by hand: aria-describedby="<id>-note", and aria-labelledby="<id>-label <id>" for a button.
  • control="input" for a native input or textarea. The label is a real <label> that points at it.
  • control="button" for a trigger whose text is its value, like a picker. It reads as the label and the value: "Room, Boardroom". Pressing the label focuses the button and does not open it.
  • control="custom" for a group or an editor. The label names it. Pass onLabel to say what pressing the label focuses.
  • Use FieldButton for anything that opens a list or a picker from a field. It has the field's box and states, and a chevron that turns over while open.
  • Put a saving status in status, and a counter like "12/80" in aside.

Avoid it for

  • A label you hide. Every field needs a name people can see.
  • Putting the error only in the red edge. Pass it as error, so it is said and shown in words.
  • A form section with several fields. Use a heading or a fieldset for the section, and a frame for each field.

Props

  • id

    Required. The control's id. The label and the note take ids built from it.

    string

  • label

    Names the control. Pressing it focuses the control.

    ReactNode

  • hint

    A short note under the control that describes it.

    ReactNode

  • error

    Replaces the hint while set, in red. Announced once each time a new message appears.

    ReactNode

  • size

    Label and note text size, and the gaps. Match the control's size.

    "sm" | "md" | "lg" · default "md"

  • control

    What the frame holds, which decides how the label names it.

    "input" | "button" | "custom" · default "input"

  • onLabel

    What pressing a label that is not a native label does. By default it focuses the element with the id.

    () => void

  • aside

    Ends the note line, like a counter. Muted, with figures that keep their width.

    ReactNode

  • status

    Ends the label line, like a saving status. Kept to the label's height.

    ReactNode

  • disabled

    Dims the label to match a disabled control.

    boolean · default false

  • className

    Extra classes for the frame. Later classes win.

    string

Accessibility

  • For control="input" the label is a native <label htmlFor>, so clicking it focuses the input and screen readers read it as the name.
  • For a button or a group the label is plain text with an id, named through aria-labelledby. A native label would press the button.
  • The hint or error has the id <id>-note. fieldIds points the control's aria-describedby at it.
  • The error has role="alert". It mounts fresh for each new message, so screen readers say it once when it appears.
  • The error color is the danger tone's soft text, which the theme tests hold to 4.5:1. The words carry the meaning, not only the color.
  • FieldButton sets aria-haspopup, aria-expanded, and aria-controls while open. A popup trigger, like PopoverTrigger, sets them itself instead.

Examples

A text field with a hint

<FieldFrame id="shirt" label="Shirt size" hint="Shirts run small.">
  <Input id="shirt" aria-describedby="shirt-note" />
</FieldFrame>

An error in place of the hint

<FieldFrame id="handle" label="Username" hint="Letters and numbers." error="That name is taken.">
  <Input id="handle" defaultValue="admin" {...fieldIds("handle", { hint: "Letters and numbers.", error: "That name is taken." })} />
</FieldFrame>

A picker trigger

<FieldFrame id="room" label="Room" control="button">
  <FieldButton id="room" filled {...fieldIds("room", { label: "Room", control: "button" })}>
    Boardroom
  </FieldButton>
</FieldFrame>