ComponentProduct
Field
Form field layout with a label, required indicator, help text, and error message.
$ npx context-ds add fieldUsage
Use it for
Every form control.
Avoid
- Placeholder as the only label
Dependencies: label
Example
examples/field.tsx
"use client";import { useState } from "react";import { Field } from "@/components/ui/field";import { Input } from "@/components/ui/input";export function Product() { const [email, setEmail] = useState("anwar@"); const [valid, setValid] = useState(false); return ( <div className="grid w-full max-w-sm gap-5"> <Field label="Webhook URL" htmlFor="hook" help="We POST every change here." required> <Input id="hook" type="url" placeholder="https://acme.com/hooks/context" /> </Field> <Field label="Email" htmlFor="email" error={valid ? undefined : "Enter a valid email."} help="We'll send monitor updates here."> <Input id="email" type="email" required value={email} onChange={(event) => { setEmail(event.target.value); setValid(event.target.validity.valid); }} /> </Field> </div> );}Source
components/ui/field.tsx
import * as React from "react";import { Label } from "@/components/ui/label";import { cn } from "@/lib/utils";// The dashboard's form field: the label above in body-sm (not muted), a red asterisk when required, help or an error// below in caption.function Field({ label, htmlFor, help, error, required = false, className, children,}: { label: React.ReactNode; htmlFor?: string; help?: React.ReactNode; error?: React.ReactNode; required?: boolean; className?: string; children: React.ReactNode;}) { const generatedId = React.useId(); const control = React.Children.toArray(children).find((child) => React.isValidElement<{ id?: string }>(child)); const id = htmlFor ?? (React.isValidElement<{ id?: string }>(control) ? control.props.id : undefined) ?? generatedId; const messageId = `${id}-message`; return ( <div data-slot="field" data-invalid={error ? true : undefined} className={cn("flex flex-col gap-2", className)}> <Label htmlFor={id} className="block"> {label} {required && ( <> <span aria-hidden className="ml-0.5 text-danger"> * </span> <span className="sr-only"> (required)</span> </> )} </Label> {React.Children.map(children, (child) => { if (!React.isValidElement<React.InputHTMLAttributes<HTMLInputElement>>(child)) return child; return React.cloneElement(child, { id, ...(required ? { required: true } : {}), "aria-invalid": error ? true : child.props["aria-invalid"], "aria-describedby": [child.props["aria-describedby"], (error || help) && messageId].filter(Boolean).join(" ") || undefined, }); })} <p id={messageId} aria-live="polite" className={cn("text-caption empty:hidden", error ? "text-danger" : "text-fg-subtle")}>{error ?? help}</p> </div> );}export { Field };Reference: context-dashboard c0ddf7a src/components/ui/form.tsx.