@cf/form · beta
Form
A thin wrapper over Base UI's Field (root, label, description, error) for fields that want Base UI's own validation state.
Submit with anything, then with open-sesame. The error text is my own. invalid on Field marks the field, but in Base UI 1.7 FieldError only renders from computed validity, so I pass match to force it while there's an error.
This is the older of the two field sets, and it overlaps with field. Field here is Base UI's Field.Root exported as is; the label, description and error parts only add type styles.
Only the access form uses it, and even there the input is the plain Input, so the form sets htmlFor, aria-invalid and data-invalid by hand and writes its own error text. I'm keeping it while that form is the only caller. The honest direction is to fold it into field or wire Input up as a Field.Control, and until one of those happens I've marked it beta.
import { Field, FieldLabel } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
<Field data-invalid={Boolean(error)}>
<FieldLabel htmlFor="access-code">Access code</FieldLabel>
<Input aria-invalid={Boolean(error)} id="access-code" type="password" />
</Field>Rules
The same rules ship in DESIGN.md and the MCP server, for the agents building with this.
- must
Don't import
Field,FieldLabel,FieldDescriptionorFieldErrorfrom bothformandfieldin the same file; they share names but are different components.dont-mix-with-field
- should
Reach for the
fieldcomponent for new forms, and useformonly where you want Base UI'sField.Rootvalidity handling.prefer-field-for-layout
- must
Set
htmlForonFieldLabeland a matchingidon the input, becauseInputis a plain<input>and not Base UI'sField.Control, so Base UI can't wire the label up itself.explicit-html-for
- should
Know that this
FieldErrorrenders from Base UI's validity state, which a plainInputdoesn't report; show custom error text yourself, as the access form does, or giveFieldErroramatchto force it. PassinginvalidtoFielddoes not show it.error-needs-validity
Install
bunx shadcn@latest add @cf/formNeeds the @cf registry in your components.json once. The theme and any sibling components come along automatically.
Source
form.tsxShowHide
"use client";
import { Field as FieldPrimitive } from "@base-ui/react/field";
import { cn } from "cn";
import * as React from "react";
const Field = FieldPrimitive.Root;
const FieldLabel = React.forwardRef<HTMLElement, FieldPrimitive.Label.Props>(
({ className, ...props }, ref) => (
<FieldPrimitive.Label
className={cn("font-medium text-sm leading-none", className)}
ref={ref}
{...props}
/>
)
);
FieldLabel.displayName = "FieldLabel";
const FieldDescription = React.forwardRef<
HTMLDivElement,
FieldPrimitive.Description.Props
>(({ className, ...props }, ref) => (
<FieldPrimitive.Description
className={cn("text-[0.8rem] text-muted-foreground", className)}
ref={ref}
{...props}
/>
));
FieldDescription.displayName = "FieldDescription";
const FieldError = React.forwardRef<HTMLDivElement, FieldPrimitive.Error.Props>(
({ className, ...props }, ref) => (
<FieldPrimitive.Error
className={cn("font-medium text-[0.8rem] text-destructive", className)}
ref={ref}
{...props}
/>
)
);
FieldError.displayName = "FieldError";
export { Field, FieldDescription, FieldError, FieldLabel };