implement
Field

Field

A control, its label, its hint, and its error.

Report a bug

One line describing what went wrong.

and
Email me about this

We only write when the status changes.

import { Checkbox } from "@/lib/components/ui/checkbox";
import {
	Field,
	FieldContent,
	FieldDescription,
	FieldError,
	FieldGroup,
	FieldLabel,
	FieldLegend,
	FieldSeparator,
	FieldSet,
	FieldTitle,
} from "@/lib/components/ui/field";
import { Input } from "@/lib/components/ui/input";
import { Textarea } from "@/lib/components/ui/textarea";

export default function FieldDemo() {
	return FieldSet(
		{ class: "w-full max-w-md" },
		FieldLegend("Report a bug"),
		FieldGroup(
			Field(
				FieldLabel({ for: "field-title" }, "Title"),
				Input({ id: "field-title", placeholder: "Dialog does not close on Escape" }),
				FieldDescription("One line describing what went wrong."),
			),
			Field(
				{ "data-invalid": "true" },
				FieldLabel({ for: "field-steps" }, "Steps to reproduce"),
				Textarea({ id: "field-steps", "aria-invalid": true, placeholder: "1. Open the dialog…" }),
				FieldError("Tell us how to reproduce it before submitting."),
			),
			FieldSeparator("and"),
			Field(
				{ orientation: "horizontal" },
				Checkbox({ id: "field-subscribe" }),
				FieldContent(
					FieldTitle("Email me about this"),
					FieldDescription("We only write when the status changes."),
				),
			),
		),
	);
}

Installation

npx jsrepo add @implementjs/ui/field

jsrepo pulls label and separator along with it, and installs tailwind-variants.

Usage

A field is the unit a form is actually made of. Field groups one control with everything that describes it; FieldGroup is the column they sit in; FieldSet and FieldLegend name a section.

import {
	Field,
	FieldDescription,
	FieldGroup,
	FieldLabel,
	FieldSet,
} from "@/lib/components/ui/field";

FieldSet(
	FieldLegend("Report a bug"),
	FieldGroup(
		Field(
			FieldLabel({ for: "title" }, "Title"),
			Input({ id: "title" }),
			FieldDescription("One line describing what went wrong."),
		),
	),
);

Errors

Two things mark an error, and they do different jobs: aria-invalid on the control is what gets announced, and data-invalid="true" on the field is what turns the label and the text destructive.

Field(
	{ "data-invalid": "true" },
	FieldLabel({ for: "steps" }, "Steps to reproduce"),
	Textarea({ id: "steps", "aria-invalid": true }),
	FieldError("Tell us how to reproduce it before submitting."),
);

FieldError carries role="alert", so the message is read when it appears. Rendering nothing at all when there is no error — rather than an empty element — keeps the layout from jumping.

Orientation

vertical puts the label above the control. horizontal puts it beside, which is the shape for a checkbox or a switch row — pair it with FieldContent so the title and description stack next to the control:

Field(
	{ orientation: "horizontal" },
	Checkbox({ id: "subscribe" }),
	FieldContent(
		FieldTitle("Email me about this"),
		FieldDescription("Only when the status changes."),
	),
);

responsive is the third: vertical until the form is wide enough, then horizontal. It measures FieldGroup, which is a container — so it responds to how wide the form is, not how wide the window is. A form in a narrow sidebar stays stacked even on a large screen.

Separators

FieldSeparator() is a plain rule. Pass children and the word sits on the line, which is how an "or" divider between a form and a social sign-in is built.

API Reference

FieldSet

A group of related fields. Renders a Fieldset; extra props are forwarded onto it.

FieldLegend

Names the field set. Renders a Legend; extra props are forwarded onto it.

PropTypeDefaultDescription
variant"legend" | "label""legend"label sizes it like a field label instead of a section heading.

FieldGroup

The column the fields sit in. It is a container, which is what the responsive orientation measures — so a field goes side-by-side when the form is wide enough, whatever the window is doing. Renders a Div; extra props are forwarded onto it.

Field

Sets role="group". One control and everything describing it. Setting data-invalid="true" here turns the whole field destructive at once. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
orientation"vertical" | "horizontal" | "responsive""vertical"Label above the control, beside it, or beside it once the form is wide enough. Also set as data-orientation.
Data attributeValue
[data-invalid]"true" while the field is in error
[data-orientation]"vertical" | "horizontal" | "responsive"

FieldContent

Title and description together, for a horizontal field with the control beside them. Renders a Div; extra props are forwarded onto it.

FieldLabel

The field's label. Wraps Label, and adds the rules for a label with a whole control nested inside it — a checkbox row, where the label is the click target for the box. Renders a Label; extra props are forwarded onto it.

FieldTitle

A heading inside a label that wraps a control. Renders a Div; extra props are forwarded onto it.

FieldDescription

The hint under the control. Renders a P; extra props are forwarded onto it.

FieldSeparator

A rule between fields. Pass children to sit a word on the line. Renders a Div; extra props are forwarded onto it.

FieldError

Sets role="alert". Give the control aria-invalid and the field data-invalid="true" so the styling and the announcement agree. Renders a Div; extra props are forwarded onto it.