This is your public display name.
Please enter a valid email address.
import { Input } from "@/components/ui/input"
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
export default function FieldDemo() {
return (
<FieldGroup className="w-full max-w-sm">
<Field>
<FieldLabel htmlFor="field-demo-username">Username</FieldLabel>
<Input id="field-demo-username" placeholder="shadcn" />
<FieldDescription>This is your public display name.</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="field-demo-email">Email</FieldLabel>
<Input
id="field-demo-email"
type="email"
aria-invalid
defaultValue="not-an-email"
/>
<FieldError>Please enter a valid email address.</FieldError>
</Field>
</FieldGroup>
)
}Installation
npx logic2b@latest add fieldWorking with an AI assistant? Copy this prompt into Claude Code, Cursor or Copilot — it carries the install steps, a no-CLI fallback and a verification checklist.
Add the "field" item from the logic2b ui registry (https://ui.logic2b.com) to this project. Follow every step and verify at the end. ## Steps 1. If the project has no `components.json` yet, set up logic2b ui first: ```bash npx logic2b@latest init ``` 2. Install the item (the CLI resolves registry dependencies automatically): ```bash npx logic2b@latest add field ``` 3. Install any npm dependencies the command prints. ## If the CLI is not available Do it manually from the registry payload: 1. Fetch https://ui.logic2b.com/r/field.json. 2. Write every entry in `files[]` to its mapped project path (see below). 3. Repeat (recursively) for each name in `registryDependencies`. 4. Install every package listed in `dependencies` across those payloads. ## Registry reference - Index of every item: https://ui.logic2b.com/r/index.json - Item payload (full source): https://ui.logic2b.com/r/<name>.json - Usage examples per item: https://ui.logic2b.com/r/demos/index.json → https://ui.logic2b.com/r/demos/<demo>.json - Docs index for agents: https://ui.logic2b.com/llms.txt (full docs: https://ui.logic2b.com/llms-full.txt) - Every docs page is Markdown when you append `.md` to its URL. Registry file paths map to project paths like this: - `ui/*` → `@/components/ui/*` - `blocks/<name>/*` → `@/components/*` - `charts/*` → `@/components/charts/*` - `hooks/*` → `@/hooks/*` - `lib/utils.ts` → `@/lib/utils.ts` - `theme.css` → next to the project's Tailwind entry stylesheet ## Conventions (do not break these) - Tailwind CSS v4 (CSS-first config; no tailwind.config.js needed) and React 19. - All colors go through semantic tokens (`bg-primary`, `text-muted-foreground`, `border-border`, …). Never hardcode hex/oklch values in components. - Dark mode is class-based: toggle `.dark` on `<html>`. - Icons come from lucide-react. ## Verify before finishing 1. The dev server starts and the app renders without console errors. 2. TypeScript compiles (`tsc --noEmit` or the framework's check). 3. Components use the theme: a `<Button>` shows the primary token color, and toggling `.dark` on `<html>` switches the palette.
Install the dependencies:
npm install class-variance-authorityCopy into ui/field.tsx:
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
return (
<fieldset
data-slot="field-set"
className={cn(
"flex flex-col gap-6",
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
className
)}
{...props}
/>
)
}
function FieldLegend({
className,
variant = "legend",
...props
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
return (
<legend
data-slot="field-legend"
data-variant={variant}
className={cn(
"mb-3 font-medium",
"data-[variant=legend]:text-base",
"data-[variant=label]:text-sm",
className
)}
{...props}
/>
)
}
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-group"
className={cn(
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
className
)}
{...props}
/>
)
}
const fieldVariants = cva(
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
{
variants: {
orientation: {
vertical: "flex-col [&>*]:w-full",
horizontal: "flex-row items-center",
responsive:
"flex-col @md/field-group:flex-row @md/field-group:items-center [&>.sr-only]:@md/field-group:not-sr-only",
},
},
defaultVariants: {
orientation: "vertical",
},
}
)
function Field({
className,
orientation = "vertical",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
return (
<div
role="group"
data-slot="field"
data-orientation={orientation}
className={cn(fieldVariants({ orientation, className }))}
{...props}
/>
)
}
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-content"
className={cn(
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
className
)}
{...props}
/>
)
}
function FieldLabel({
className,
...props
}: React.ComponentProps<typeof Label>) {
return (
<Label
data-slot="field-label"
className={cn(
"flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 group-data-[orientation=horizontal]/field:has-[>[data-slot=field]]:w-full",
className
)}
{...props}
/>
)
}
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-title"
className={cn(
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
className
)}
{...props}
/>
)
}
function FieldDescription({
className,
...props
}: React.ComponentProps<"p">) {
return (
<p
data-slot="field-description"
className={cn(
"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
"last:mt-0",
className
)}
{...props}
/>
)
}
function FieldSeparator({
children,
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="field-separator"
data-content={!!children}
className={cn(
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
className
)}
{...props}
>
<Separator className="absolute inset-0 top-1/2" />
{children && (
<span
data-slot="field-separator-content"
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
>
{children}
</span>
)}
</div>
)
}
function FieldError({
className,
children,
errors,
...props
}: React.ComponentProps<"div"> & {
errors?: Array<{ message?: string } | undefined>
}) {
const content = React.useMemo(() => {
if (children) return children
if (!errors) return null
const filtered = errors.filter(
(error): error is { message?: string } => Boolean(error?.message)
)
if (filtered.length === 1) return filtered[0].message
return (
<ul className="ml-4 flex list-disc flex-col gap-1">
{filtered.map((error, index) => (
<li key={index}>{error.message}</li>
))}
</ul>
)
}, [children, errors])
if (!content) return null
return (
<div
role="alert"
data-slot="field-error"
className={cn("text-destructive text-sm font-normal", className)}
{...props}
>
{content}
</div>
)
}
export {
Field,
FieldLabel,
FieldDescription,
FieldError,
FieldGroup,
FieldSeparator,
FieldSet,
FieldLegend,
FieldContent,
FieldTitle,
}Usage
import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"
<FieldGroup>
<Field>
<FieldLabel htmlFor="username">Username</FieldLabel>
<Input id="username" placeholder="shadcn" />
<FieldDescription>This is your public display name.</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input id="email" type="email" aria-invalid />
<FieldError>Please enter a valid email address.</FieldError>
</Field>
</FieldGroup>
Use field for simple forms or to wrap a single control without react-hook-form.
For forms that need schema validation wired to field state, use the
form component (react-hook-form + zod) instead.
API
| Prop | Type | Default |
|---|---|---|
orientation (Field) |
vertical | horizontal | responsive |
vertical |
variant (FieldLegend) |
legend | label |
legend |
errors (FieldError) |
Array<{ message?: string } | undefined> |
— |
Examples
Horizontal
Pair a control with a label and description side by side.
Require a second step when signing in.
import {
Field,
FieldContent,
FieldDescription,
FieldLabel,
} from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"
export default function FieldHorizontalDemo() {
return (
<Field orientation="horizontal" className="w-full max-w-sm">
<FieldContent>
<FieldLabel htmlFor="field-horizontal-2fa">
Two-factor authentication
</FieldLabel>
<FieldDescription>
Require a second step when signing in.
</FieldDescription>
</FieldContent>
<Switch id="field-horizontal-2fa" />
</Field>
)
}Field Set
Group related fields under a FieldLegend.
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
export default function FieldSetDemo() {
return (
<FieldSet className="w-full max-w-sm">
<FieldLegend>Shipping address</FieldLegend>
<FieldDescription>Where should we send your order?</FieldDescription>
<FieldGroup>
<Field>
<FieldLabel htmlFor="field-set-street">Street</FieldLabel>
<Input id="field-set-street" placeholder="123 Main St" />
</Field>
<FieldSeparator />
<Field>
<FieldLabel htmlFor="field-set-city">City</FieldLabel>
<Input id="field-set-city" placeholder="San Francisco" />
</Field>
</FieldGroup>
</FieldSet>
)
}