@cf/ui

@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.

Try anything, then open-sesame.

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, FieldDescription or FieldError from both form and field in the same file; they share names but are different components.

    dont-mix-with-field

  • should

    Reach for the field component for new forms, and use form only where you want Base UI's Field.Root validity handling.

    prefer-field-for-layout

  • must

    Set htmlFor on FieldLabel and a matching id on the input, because Input is a plain <input> and not Base UI's Field.Control, so Base UI can't wire the label up itself.

    explicit-html-for

  • should

    Know that this FieldError renders from Base UI's validity state, which a plain Input doesn't report; show custom error text yourself, as the access form does, or give FieldError a match to force it. Passing invalid to Field does not show it.

    error-needs-validity

Install

bunx shadcn@latest add @cf/form

Needs the @cf registry in your components.json once. The theme and any sibling components come along automatically.

Source

form.tsxShow
"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 };