# Field

Combine labels, controls, and help text to compose accessible form fields and grouped inputs.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Field, FieldDescription, FieldGroup, FieldLabel, FieldSeparator } from "@/components/ui/field"
import { FieldsetLegend, Fieldset } from "@/components/ui/fieldset"
import { Input } from "@/components/ui/input"
import { space } from "@/theme/tokens.stylex"

const styles = stylex.create({
  form: { width: "100%", maxWidth: "28rem" },
  columns: { display: "grid", gridTemplateColumns: "repeat(3, minmax(0, 1fr))", gap: space.s4 },
})

export default function FieldDemo() {
  return (
    <form {...stylex.props(styles.form)} onSubmit={(event) => event.preventDefault()}>
      <FieldGroup>
        <Fieldset>
          <FieldsetLegend>Payment Method</FieldsetLegend>
          <FieldDescription>All transactions are secure and encrypted</FieldDescription>
          <FieldGroup>
            <Field>
              <FieldLabel>Name on Card</FieldLabel>
              <Input id="checkout-card-name" placeholder="Evil Rabbit" required />
            </Field>
            <Field>
              <FieldLabel>Card Number</FieldLabel>
              <Input id="checkout-card-number" placeholder="1234 5678 9012 3456" required />
              <FieldDescription>Enter your 16-digit card number</FieldDescription>
            </Field>
            <div {...stylex.props(styles.columns)}>
              <Field>
                <FieldLabel>Month</FieldLabel>
                <Input id="checkout-exp-month" placeholder="MM" required />
              </Field>
              <Field>
                <FieldLabel>Year</FieldLabel>
                <Input id="checkout-exp-year" placeholder="YYYY" required />
              </Field>
              <Field>
                <FieldLabel>CVV</FieldLabel>
                <Input id="checkout-cvv" placeholder="123" required />
              </Field>
            </div>
          </FieldGroup>
        </Fieldset>
        <FieldSeparator />
        <Fieldset>
          <FieldsetLegend>Billing Address</FieldsetLegend>
          <FieldDescription>
            The billing address associated with your payment method
          </FieldDescription>
          <FieldGroup>
            <Field orientation="horizontal">
              <Checkbox id="checkout-same-as-shipping" defaultChecked />
              <FieldLabel>Same as shipping address</FieldLabel>
            </Field>
          </FieldGroup>
        </Fieldset>
        <Field orientation="horizontal">
          <Button type="submit">Submit</Button>
          <Button variant="outline" type="button">
            Cancel
          </Button>
        </Field>
      </FieldGroup>
    </form>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex clsx
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation), and paste `states.ts` from the same page,
   which is where a choice card's focus ring comes from. The field builds on
   [Label](https://carbon.style/docs/components/label), [Separator](https://carbon.style/docs/components/separator) and
   [Stack](https://carbon.style/docs/components/stack), so paste those first. It also needs `interop.css`, which styles a link inside a
   description.

   Paste `field-state.ts` and `state-styles.ts` from the same installation page.

3. Copy and paste the following code into your project.

   ```tsx title="components/ui/field.tsx"
   "use client"

   import { useRender } from "@base-ui/react/use-render"
   import type { CSSProperties, ReactNode } from "react"
   import { Children, createContext, isValidElement, use, useState } from "react"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"
   import { clsx } from "clsx"

   import { Field as FieldPrimitive } from "@base-ui/react/field"
   import { FieldStateContext } from "@/lib/field-state"
   import { stateStyles } from "@/lib/state-styles"
   import { labelStyles } from "@/components/ui/label"
   import { Separator } from "@/components/ui/separator"
   import { VStack, type SpacingStep } from "@/components/ui/stack"
   import { states } from "@/theme/states"
   import {
     color,
     duration,
     ease,
     leading,
     radius,
     shadow,
     space,
     text,
   } from "@/theme/tokens.stylex"

   type FieldOrientation = "vertical" | "horizontal" | "responsive"

   type FieldState = {
     orientation: FieldOrientation
     inContent: boolean
   }

   const FieldContext = createContext<FieldState | null>(null)

   const GROUP_MD = "@container field-group (width >= 28rem)"

   const styles = stylex.create({
     group: {
       width: "100%",
       containerType: "inline-size",
       containerName: "field-group",
     },
     field: {
       display: "flex",
       width: "100%",
       gap: space.s2,
     },
     horizontalWithContent: { alignItems: "flex-start" },
     responsiveWithContent: { alignItems: { default: "stretch", [GROUP_MD]: "flex-start" } },
     invalid: { color: color.destructive },
     control: { width: { default: "100%", [GROUP_MD]: "auto" } },
     content: {
       display: "flex",
       flexDirection: "column",
       gap: space.s1,
       flexGrow: 1,
       flexShrink: 1,
       flexBasis: "0%",
     },
     label: { width: "fit-content" },
     labelGrow: { flexGrow: 1, flexShrink: 1, flexBasis: "auto" },
     card: {
       width: "100%",
       flexDirection: "column",
       padding: space.s2_5,
       borderRadius: radius.lg,
       borderWidth: 1,
       borderStyle: "solid",
       "--carbon-border-color": color.transparent,
       borderColor: "var(--carbon-border-color)",
       backgroundColor: color.card,
       "--carbon-field-card-surface": shadow.raised,
       boxShadow: "var(--carbon-field-card-surface)",
       transitionProperty: "color, background-color, border-color, box-shadow",
       transitionDuration: duration.color,
       transitionTimingFunction: ease.standard,
     },
     cardChecked: { "--carbon-border-color": color.foreground },
     cardFocused: { boxShadow: `var(--carbon-field-card-surface), ${shadow.ring}` },
     title: {
       display: "flex",
       alignItems: "center",
       gap: space.s2,
       width: "fit-content",
       fontSize: text.sm,
       lineHeight: 1,
       fontWeight: 500,
       "--carbon-icon-size": space.s4,
     },
     dimmed: { opacity: 0.5 },
     description: {
       color: color.mutedForeground,
       fontSize: text.sm,
       lineHeight: leading.sm,
       fontWeight: 400,
     },
     descriptionBalanced: { textWrap: "balance" },
     separator: {
       position: "relative",
       height: space.s5,
       marginBlock: `calc(-1 * ${space.s2})`,
       fontSize: text.sm,
       lineHeight: leading.sm,
     },
     separatorLine: {
       position: "absolute",
       insetInline: 0,
       top: "50%",
     },
     separatorContent: {
       position: "relative",
       display: "block",
       width: "fit-content",
       marginInline: "auto",
       paddingInline: space.s2,
       backgroundColor: color.background,
       color: color.mutedForeground,
     },
     error: {
       color: color.destructive,
       fontSize: text.sm,
       lineHeight: leading.sm,
       fontWeight: 400,
     },
     errorList: {
       display: "flex",
       flexDirection: "column",
       gap: space.s1,
       marginInlineStart: space.s4,
       listStyleType: "disc",
     },
   })

   const orientations = stylex.create({
     vertical: { flexDirection: "column" },
     horizontal: { flexDirection: "row", alignItems: "center" },
     responsive: {
       flexDirection: { default: "column", [GROUP_MD]: "row" },
       alignItems: { default: "stretch", [GROUP_MD]: "center" },
     },
   })

   const fieldStyles = {
     field: styles.field,
     orientations,
     invalid: styles.invalid,
     control: styles.control,
     card: styles.card,
     description: styles.description,
     error: styles.error,
   }

   type StyleProps = {
     xstyle?: StyleXStyles
     className?: string
   }

   type FieldGroupProps = useRender.ComponentProps<"div"> &
     StyleProps & {
       gap?: SpacingStep
     }

   function FieldGroup({ gap = 5, xstyle, className, ...props }: FieldGroupProps) {
     return (
       <VStack
         data-slot="field-group"
         gap={gap}
         xstyle={[styles.group, xstyle]}
         className={className}
         {...props}
       />
     )
   }

   type FieldProps = Omit<FieldPrimitive.Root.Props, "className" | "style"> &
     StyleProps & { style?: CSSProperties; orientation?: FieldOrientation }

   function Field({
     render,
     orientation = "vertical",
     xstyle,
     className,
     style,
     children,
     ...props
   }: FieldProps) {
     const hasContent = Children.toArray(children).some(
       (child) => isValidElement(child) && child.type === FieldContent,
     )
     return (
       <FieldContext value={{ orientation, inContent: false }}>
         <FieldPrimitive.Root
           data-slot="field"
           data-orientation={orientation}
           {...props}
           {...stateStyles(
             (state) => [
               styles.field,
               orientations[orientation],
               hasContent && orientation === "horizontal" && styles.horizontalWithContent,
               hasContent && orientation === "responsive" && styles.responsiveWithContent,
               (state.valid === false || props.invalid) && styles.invalid,
               xstyle,
             ],
             { className, style },
           )}
           render={(rootProps, state) => (
             <FieldStateContext value={{ ...state, valid: props.invalid ? false : state.valid }}>
               <FieldHost {...rootProps} render={render} fieldState={state} />
             </FieldStateContext>
           )}
         >
           {children}
         </FieldPrimitive.Root>
       </FieldContext>
     )
   }

   function FieldHost({
     render,
     ref,
     fieldState,
     ...props
   }: useRender.ComponentProps<"div", FieldPrimitive.Root.State> & {
     fieldState: FieldPrimitive.Root.State
   }) {
     return useRender({ defaultTagName: "div", render, ref, state: { ...fieldState }, props })
   }

   function FieldContent({
     render,
     ref,
     style,
     xstyle,
     className,
     children,
     ...props
   }: useRender.ComponentProps<"div"> & StyleProps) {
     const field = use(FieldContext)
     const sx = stylex.props(styles.content, xstyle)

     return (
       <FieldContext value={field && { ...field, inContent: true }}>
         {useRender({
           defaultTagName: "div",
           render,
           ref,
           props: {
             "data-slot": "field-content",
             ...sx,
             ...props,
             className: clsx(sx.className, className),
             style: { ...sx.style, ...style },
             children,
           },
         })}
       </FieldContext>
     )
   }

   type FieldLabelProps = Omit<FieldPrimitive.Label.Props, "className" | "style"> &
     StyleProps & {
       style?: CSSProperties
       disabled?: boolean
       checked?: boolean
     }

   function FieldItem({
     xstyle,
     className,
     style,
     ...props
   }: Omit<FieldPrimitive.Item.Props, "className" | "style"> &
     StyleProps & { style?: CSSProperties }) {
     return (
       <FieldPrimitive.Item
         data-slot="field-item"
         {...props}
         {...stateStyles(() => [xstyle], { className, style })}
       />
     )
   }

   function FieldLabel({
     style,
     checked,
     disabled,
     xstyle,
     className,
     children,
     onFocus,
     onBlur,
     ...props
   }: FieldLabelProps) {
     const field = use(FieldContext)
     const [focused, setFocused] = useState(false)
     const card = Children.toArray(children).some(
       (child) => isValidElement(child) && child.type === Field,
     )
     const grows = field !== null && !field.inContent && field.orientation !== "vertical"
     const resolve = (isDisabled: boolean) => [
       labelStyles.base,
       isDisabled && labelStyles.disabled,
       styles.label,
       grows && styles.labelGrow,
       card && styles.card,
       card && checked && styles.cardChecked,
       card && focused && states.ring,
       card && focused && styles.cardFocused,
       xstyle,
     ]
     const handlers = {
       onFocus: (event: Parameters<NonNullable<FieldPrimitive.Label.Props["onFocus"]>>[0]) => {
         onFocus?.(event)
         if (card) setFocused(event.target.matches(":focus-visible"))
       },
       onBlur: (event: Parameters<NonNullable<FieldPrimitive.Label.Props["onBlur"]>>[0]) => {
         onBlur?.(event)
         setFocused(false)
       },
     }
     const sx = stylex.props(...resolve(!!disabled))
     const { render, ref, nativeLabel, ...hostProps } = props
     const host = useRender({
       defaultTagName: "label",
       render,
       ref,
       enabled: !field,
       state: {
         disabled: !!disabled,
         valid: null,
         dirty: false,
         touched: false,
         filled: false,
         focused,
       },
       props: {
         ...hostProps,
         ...handlers,
         "data-slot": "field-label",
         "data-checked": card && checked ? "" : undefined,
         className: clsx("carbon-icons", sx.className, className),
         style: { ...sx.style, ...style },
         children,
       },
     })
     if (!field) return host
     return (
       <FieldPrimitive.Label
         data-slot="field-label"
         data-checked={card && checked ? "" : undefined}
         {...props}
         nativeLabel={nativeLabel}
         {...handlers}
         {...stateStyles((state) => resolve(disabled ?? state.disabled), {
           className,
           style,
           hook: "carbon-icons",
         })}
       >
         {children}
       </FieldPrimitive.Label>
     )
   }

   function FieldTitle({
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: useRender.ComponentProps<"div"> & StyleProps) {
     const state = use(FieldStateContext)
     const sx = stylex.props(styles.title, state?.disabled && styles.dimmed, xstyle)

     return useRender({
       defaultTagName: "div",
       render,
       ref,
       props: {
         "data-slot": "field-title",
         ...sx,
         ...props,
         className: clsx("carbon-icons", sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   function FieldDescription({
     xstyle,
     className,
     style,
     ...props
   }: Omit<FieldPrimitive.Description.Props, "className" | "style"> &
     StyleProps & { style?: CSSProperties }) {
     const field = use(FieldContext)
     const { render, ref, ...hostProps } = props
     const sx = stylex.props(styles.description, xstyle)
     const host = useRender({
       defaultTagName: "p",
       render,
       ref,
       enabled: !field,
       state: {
         disabled: false,
         valid: null,
         dirty: false,
         touched: false,
         filled: false,
         focused: false,
       },
       props: {
         ...hostProps,
         "data-slot": "field-description",
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
     if (!field) return host
     return (
       <FieldPrimitive.Description
         data-slot="field-description"
         {...props}
         {...stateStyles(
           () => [
             styles.description,
             field.orientation !== "vertical" && styles.descriptionBalanced,
             xstyle,
           ],
           { className, style },
         )}
       />
     )
   }

   function FieldSeparator({
     render,
     ref,
     style,
     children,
     xstyle,
     className,
     ...props
   }: useRender.ComponentProps<"div"> & StyleProps) {
     const sx = stylex.props(styles.separator, xstyle)

     return useRender({
       defaultTagName: "div",
       render,
       ref,
       state: { content: !!children },
       props: {
         "data-slot": "field-separator",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
         children: (
           <>
             <Separator data-slot="field-separator-line" xstyle={styles.separatorLine} />
             {children && (
               <span data-slot="field-separator-content" {...stylex.props(styles.separatorContent)}>
                 {children}
               </span>
             )}
           </>
         ),
       },
     })
   }

   type FieldErrorProps = Omit<FieldPrimitive.Error.Props, "className" | "style"> &
     StyleProps & { style?: CSSProperties; errors?: Array<{ message?: string } | undefined> }
   function FieldError({ errors, children, xstyle, className, style, ...props }: FieldErrorProps) {
     const field = use(FieldContext)
     const fieldState = use(FieldStateContext)
     const content = children ?? errorContent(errors)
     const { render, ref, match, ...hostProps } = props
     const sx = stylex.props(styles.error, xstyle)
     const host = useRender({
       defaultTagName: "div",
       render,
       ref,
       enabled: !field && !!content,
       state: {
         disabled: false,
         valid: false,
         dirty: false,
         touched: false,
         filled: false,
         focused: false,
         transitionStatus: undefined,
       },
       props: {
         ...hostProps,
         role: "alert",
         "data-slot": "field-error",
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
         children: content,
       },
     })
     if (!field) return host
     return (
       <FieldPrimitive.Error
         data-slot="field-error"
         role="alert"
         {...props}
         match={
           match ??
           (errors !== undefined
             ? !!content
             : content != null && fieldState?.valid === false && !fieldState.disabled
               ? true
               : undefined)
         }
         {...stateStyles(() => [styles.error, xstyle], { className, style })}
         {...(content != null ? { children: content } : {})}
       />
     )
   }

   const FieldValidity = FieldPrimitive.Validity

   function errorContent(errors: FieldErrorProps["errors"]): ReactNode {
     if (!errors?.length) return null

     const messages = [...new Set(errors.map((error) => error?.message).filter(Boolean))]
     if (messages.length === 0) return null
     if (messages.length === 1) return messages[0]

     return (
       <ul {...stylex.props(styles.errorList)}>
         {messages.map((message) => (
           <li key={message}>{message}</li>
         ))}
       </ul>
     )
   }

   export {
     Field,
     FieldContent,
     FieldItem,
     FieldDescription,
     FieldError,
     FieldGroup,
     FieldLabel,
     FieldSeparator,
     FieldTitle,
     FieldValidity,
     fieldStyles,
     type FieldOrientation,
   }
   ```

4. Update the import paths to match your project setup.

Use [Fieldset](https://carbon.style/docs/components/fieldset) to group related fields.

## Usage

```tsx
import {
  Field,
  FieldContent,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
  FieldSeparator,
  FieldTitle,
} from "@/components/ui/field"
import { FieldsetLegend, Fieldset } from "@/components/ui/fieldset"
```

```tsx
<Fieldset>
  <FieldsetLegend>Profile</FieldsetLegend>
  <FieldDescription>This appears on invoices and emails.</FieldDescription>
  <FieldGroup>
    <Field>
      <FieldLabel>Full name</FieldLabel>
      <Input id="name" autoComplete="off" placeholder="Evil Rabbit" />
      <FieldDescription>This appears on invoices and emails.</FieldDescription>
    </Field>
    <Field invalid>
      <FieldLabel>Username</FieldLabel>
      <Input id="username" autoComplete="off" />
      <FieldError>Choose another username.</FieldError>
    </Field>
    <Field orientation="horizontal">
      <Checkbox id="newsletter" />
      <FieldLabel>Subscribe to the newsletter</FieldLabel>
    </Field>
  </FieldGroup>
</Fieldset>
```

Use carbon controls inside `Field` to inherit labels, descriptions, validation and disabled state.

## Composition

### Field

A single control with a label, helper text and validation.

```text
Field
├── FieldLabel
├── Input / Checkbox
├── FieldDescription
└── FieldError
```

### FieldGroup

Related fields in one group. Use `FieldSeparator` between sections when needed.

```text
FieldGroup
├── Field
├── FieldSeparator
└── Field
```

### Fieldset

Semantic grouping with a legend and description, usually containing a
`FieldGroup`.

```text
Fieldset
├── FieldsetLegend
├── FieldDescription
└── FieldGroup
    ├── Field
    └── Field
```

## Anatomy

```tsx
<Field>
  <FieldLabel>Label</FieldLabel>
  {/* Input, Checkbox, etc. */}
  <FieldDescription>Optional helper text.</FieldDescription>
  <FieldError>Validation message.</FieldError>
</Field>
```

- `Field` is the wrapper for a single field. It publishes its `orientation`,
  `invalid` and `disabled` state to the parts inside it.
- `FieldContent` is a column that groups the label and description beside a
  control. You do not need it if there is no description.
- Wrap related fields with `FieldGroup`, and use `Fieldset` with `FieldsetLegend`
  for semantic grouping.

## Form

See [React Hook Form](https://carbon.style/docs/forms/react-hook-form) for building a form with
`Field`, schema validation and per-field errors.

## Input

```tsx
import * as stylex from "@stylexjs/stylex"
import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Fieldset } from "@/components/ui/fieldset"
import { Input } from "@/components/ui/input"

const styles = stylex.create({
  fieldset: { width: "100%", maxWidth: "20rem" },
})

export default function FieldInput() {
  return (
    <Fieldset xstyle={styles.fieldset}>
      <FieldGroup>
        <Field>
          <FieldLabel>Username</FieldLabel>
          <Input id="field-input-username" type="text" placeholder="Max Leiter" />
          <FieldDescription>Choose a unique username for your account.</FieldDescription>
        </Field>
        <Field>
          <FieldLabel>Password</FieldLabel>
          <FieldDescription>Must be at least 8 characters long.</FieldDescription>
          <Input id="field-input-password" type="password" placeholder="••••••••" />
        </Field>
      </FieldGroup>
    </Fieldset>
  )
}
```

## Fieldset

Use `Fieldset` and `FieldsetLegend` to group fields under a heading. `FieldsetLegend`
takes `variant="label"` to size the heading like a label, for a fieldset nested
inside a form.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field"
import { FieldsetLegend, Fieldset } from "@/components/ui/fieldset"
import { Input } from "@/components/ui/input"
import { space } from "@/theme/tokens.stylex"

const styles = stylex.create({
  fieldset: { width: "100%", maxWidth: "24rem" },
  columns: { display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 1fr))", gap: space.s4 },
})

export default function FieldFieldset() {
  return (
    <Fieldset xstyle={styles.fieldset}>
      <FieldsetLegend>Address Information</FieldsetLegend>
      <FieldDescription>We need your address to deliver your order.</FieldDescription>
      <FieldGroup>
        <Field>
          <FieldLabel>Street Address</FieldLabel>
          <Input id="field-fieldset-street" type="text" placeholder="123 Main St" />
        </Field>
        <div {...stylex.props(styles.columns)}>
          <Field>
            <FieldLabel>City</FieldLabel>
            <Input id="field-fieldset-city" type="text" placeholder="New York" />
          </Field>
          <Field>
            <FieldLabel>Postal Code</FieldLabel>
            <Input id="field-fieldset-zip" type="text" placeholder="90502" />
          </Field>
        </div>
      </FieldGroup>
    </Fieldset>
  )
}
```

## Checkbox

Use `orientation="horizontal"` to put the control before the label. Wrap the
label and a description in `FieldContent` when a row has both.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Checkbox } from "@/components/ui/checkbox"
import {
  Field,
  FieldContent,
  FieldDescription,
  FieldGroup,
  FieldLabel,
  FieldSeparator,
} from "@/components/ui/field"
import { FieldsetLegend, Fieldset } from "@/components/ui/fieldset"

const styles = stylex.create({
  group: { maxWidth: "20rem" },
})

const items = [
  { id: "hard-disks", label: "Hard disks" },
  { id: "external-disks", label: "External disks" },
  { id: "cds-dvds", label: "CDs, DVDs, and iPods" },
  { id: "connected-servers", label: "Connected servers" },
]

export default function FieldCheckbox() {
  return (
    <FieldGroup xstyle={styles.group}>
      <Fieldset>
        <FieldsetLegend variant="label">Show these items on the desktop</FieldsetLegend>
        <FieldDescription>Select the items you want to show on the desktop.</FieldDescription>
        <FieldGroup gap={3}>
          {items.map((item) => (
            <Field key={item.id} orientation="horizontal">
              <Checkbox id={`finder-pref-${item.id}`} />
              <FieldLabel>{item.label}</FieldLabel>
            </Field>
          ))}
        </FieldGroup>
      </Fieldset>
      <FieldSeparator />
      <Field orientation="horizontal">
        <Checkbox id="finder-pref-sync-folders" defaultChecked />
        <FieldContent>
          <FieldLabel>Sync Desktop & Documents folders</FieldLabel>
          <FieldDescription>
            Your Desktop & Documents folders are being synced with iCloud Drive. You can access them
            from other devices.
          </FieldDescription>
        </FieldContent>
      </Field>
    </FieldGroup>
  )
}
```

## Choice card

Wrap a `Field` inside a `FieldLabel` to make the whole card select the control.
Pass `checked` to the label as well as the control.

```tsx
"use client"

import { useState } from "react"
import * as stylex from "@stylexjs/stylex"
import { Checkbox } from "@/components/ui/checkbox"
import {
  Field,
  FieldContent,
  FieldDescription,
  FieldGroup,
  FieldLabel,
  FieldTitle,
} from "@/components/ui/field"
import { FieldsetLegend, Fieldset } from "@/components/ui/fieldset"

const styles = stylex.create({
  group: { maxWidth: "20rem" },
})

const environments = [
  { id: "kubernetes", title: "Kubernetes", description: "Run GPU workloads on a K8s cluster." },
  { id: "vm", title: "Virtual Machine", description: "Access a cluster to run GPU workloads." },
]

export default function FieldChoiceCard() {
  const [selected, setSelected] = useState(["kubernetes"])

  return (
    <FieldGroup xstyle={styles.group}>
      <Fieldset>
        <FieldsetLegend variant="label">Compute Environment</FieldsetLegend>
        <FieldDescription>Select the compute environments for your cluster.</FieldDescription>
        <FieldGroup gap={3}>
          {environments.map((environment) => {
            const checked = selected.includes(environment.id)
            return (
              <FieldLabel
                key={environment.id}
                htmlFor={`compute-${environment.id}`}
                checked={checked}
              >
                <Field orientation="horizontal">
                  <FieldContent>
                    <FieldTitle>{environment.title}</FieldTitle>
                    <FieldDescription>{environment.description}</FieldDescription>
                  </FieldContent>
                  <Checkbox
                    id={`compute-${environment.id}`}
                    checked={checked}
                    onCheckedChange={(next) =>
                      setSelected(
                        next
                          ? [...selected, environment.id]
                          : selected.filter((id) => id !== environment.id),
                      )
                    }
                  />
                </Field>
              </FieldLabel>
            )
          })}
        </FieldGroup>
      </Fieldset>
    </FieldGroup>
  )
}
```

> **Note**
>
> The label draws the selected frame from its own `checked` prop, not from the
> control inside it. StyleX cannot select a parent by a child's state, and the
> `:has()` that would is off carbon's browser floor. The choice card is therefore
> a controlled pattern.

## Field group

Stack fields with `FieldGroup`. Use `FieldSeparator` to divide them, and `gap`
to tighten a list of checkboxes.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Checkbox } from "@/components/ui/checkbox"
import { Field, FieldDescription, FieldGroup, FieldLabel, FieldSeparator } from "@/components/ui/field"
import { FieldsetLegend, Fieldset } from "@/components/ui/fieldset"

const styles = stylex.create({
  group: { maxWidth: "20rem" },
})

export default function FieldGroupDemo() {
  return (
    <FieldGroup xstyle={styles.group}>
      <Fieldset>
        <FieldsetLegend variant="label">Responses</FieldsetLegend>
        <FieldDescription>
          Get notified when ChatGPT responds to requests that take time, like research or image
          generation.
        </FieldDescription>
        <FieldGroup gap={3}>
          <Field orientation="horizontal" disabled>
            <Checkbox id="notify-push" defaultChecked />
            <FieldLabel>Push notifications</FieldLabel>
          </Field>
        </FieldGroup>
      </Fieldset>
      <FieldSeparator />
      <Fieldset>
        <FieldsetLegend variant="label">Tasks</FieldsetLegend>
        <FieldDescription>
          Get notified when tasks you&apos;ve created have updates.{" "}
          <a href="https://example.com/tasks">Manage tasks</a>
        </FieldDescription>
        <FieldGroup gap={3}>
          <Field orientation="horizontal">
            <Checkbox id="notify-push-tasks" />
            <FieldLabel>Push notifications</FieldLabel>
          </Field>
          <Field orientation="horizontal">
            <Checkbox id="notify-email-tasks" />
            <FieldLabel>Email notifications</FieldLabel>
          </Field>
        </FieldGroup>
      </Fieldset>
    </FieldGroup>
  )
}
```

## Responsive layout

- **Vertical fields.** The default stacks the label, the control and the helper text.
- **Horizontal fields.** Set `orientation="horizontal"` to put the label and
  control side by side. Pair it with `FieldContent` to keep a description
  aligned under the label.
- **Responsive fields.** Set `orientation="responsive"` to stack on a narrow
  `FieldGroup` and form a row once the group is `28rem` wide. Give a control
  that stretches, such as `Input`, `xstyle={fieldStyles.control}` so its
  width becomes `auto` when the row forms.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import {
  Field,
  FieldContent,
  FieldDescription,
  FieldGroup,
  FieldLabel,
  fieldStyles,
} from "@/components/ui/field"
import { FieldsetLegend, Fieldset } from "@/components/ui/fieldset"
import { Input } from "@/components/ui/input"

const styles = stylex.create({
  form: { width: "100%", maxWidth: "32rem" },
})

export default function FieldResponsive() {
  return (
    <form {...stylex.props(styles.form)} onSubmit={(event) => event.preventDefault()}>
      <Fieldset>
        <FieldsetLegend>Profile</FieldsetLegend>
        <FieldDescription>Fill in your profile information.</FieldDescription>
        <FieldGroup>
          <Field orientation="responsive">
            <FieldContent>
              <FieldLabel>Name</FieldLabel>
              <FieldDescription>Provide your full name for identification</FieldDescription>
            </FieldContent>
            <Input
              id="profile-name"
              placeholder="Evil Rabbit"
              required
              xstyle={fieldStyles.control}
            />
          </Field>
          <Field orientation="responsive">
            <Button type="submit">Submit</Button>
            <Button type="button" variant="outline">
              Cancel
            </Button>
          </Field>
        </FieldGroup>
      </Fieldset>
    </form>
  )
}
```

> **Note**
>
> A responsive field measures the nearest `FieldGroup`, not the viewport. Outside
> a group there is no container to query and the field stays vertical.

## Validation and errors

- Set `invalid` on `Field` to turn the label and error red.
- Controls inherit validation and disabled state from `Field`.
- Render `FieldError` after the control, or inside `FieldContent`, so the
  message lines up with the field. Base UI shows it when the field is invalid.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Field, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

const styles = stylex.create({
  field: { maxWidth: "20rem" },
})

export default function FieldInvalid() {
  return (
    <Field invalid xstyle={styles.field}>
      <FieldLabel>Email</FieldLabel>
      <Input id="field-invalid-email" type="email" defaultValue="evil@rabbit" />
      <FieldDescription>We only use it to send your receipt.</FieldDescription>
      <FieldError>Enter a valid email address.</FieldError>
    </Field>
  )
}
```

```tsx
<Field invalid>
  <FieldLabel>Email</FieldLabel>
  <Input id="email" type="email" />
  <FieldError>Enter a valid email address.</FieldError>
</Field>
```

`FieldError` also takes an `errors` array, the shape a form library produces.
One message renders as text and several as a list, with duplicates removed.

```tsx
<FieldError errors={[fieldState.error]} />
```

## Inherited state

Set `invalid` and `disabled` on `Field` to style and disable its control.

```tsx
import { Field, FieldGroup, FieldLabel, FieldDescription } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Checkbox } from "@/components/ui/checkbox"
import { Switch } from "@/components/ui/switch"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import {
  Select,
  SelectTrigger,
  SelectValue,
  SelectContent,
  SelectItem,
  SelectGroup,
} from "@/components/ui/select"
import { Slider } from "@/components/ui/slider"
import { InputOTP, InputOTPSlot } from "@/components/ui/input-otp"
import { InputGroup, InputGroupAddon, InputGroupText } from "@/components/ui/input-group"
import { Combobox, ComboboxInput } from "@/components/ui/combobox"
import { VStack } from "@/components/ui/stack"

export default function FieldStates() {
  return (
    <VStack width="100%" maxWidth="24rem" gap={5}>
      {[false, true].map((disabled) => (
        <FieldGroup key={String(disabled)}>
          <Field invalid disabled={disabled}>
            <FieldLabel>Email</FieldLabel>
            <Input placeholder="Email" />
            <FieldDescription>{disabled ? "Disabled and invalid" : "Invalid"}</FieldDescription>
          </Field>
          <Field invalid disabled={disabled}>
            <FieldLabel>Message</FieldLabel>
            <Textarea placeholder="Message" />
          </Field>
          <Field invalid disabled={disabled}>
            <FieldLabel>Accept terms</FieldLabel>
            <Checkbox />
          </Field>
          <Field invalid disabled={disabled}>
            <FieldLabel>Notifications</FieldLabel>
            <Switch />
          </Field>
          <Field invalid disabled={disabled}>
            <FieldLabel>Plan</FieldLabel>
            <RadioGroup>
              <RadioGroupItem value="free" aria-label="Free" />
              <RadioGroupItem value="pro" aria-label="Pro" />
            </RadioGroup>
          </Field>
          <Field invalid disabled={disabled}>
            <FieldLabel>Fruit</FieldLabel>
            <Select>
              <SelectTrigger>
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectGroup>
                  <SelectItem value="apple">Apple</SelectItem>
                </SelectGroup>
              </SelectContent>
            </Select>
          </Field>
          <Field invalid disabled={disabled}>
            <FieldLabel>Volume</FieldLabel>
            <Slider defaultValue={25} />
          </Field>
          <Field invalid disabled={disabled}>
            <FieldLabel>Code</FieldLabel>
            <InputOTP length={2}>
              <InputOTPSlot />
              <InputOTPSlot />
            </InputOTP>
          </Field>
          <Field invalid disabled={disabled}>
            <FieldLabel>Website</FieldLabel>
            <InputGroup>
              <Input />
              <InputGroupAddon>
                <InputGroupText>https://</InputGroupText>
              </InputGroupAddon>
            </InputGroup>
          </Field>
          <Field invalid disabled={disabled}>
            <FieldLabel>Framework</FieldLabel>
            <Combobox items={["React", "Vue"]}>
              <ComboboxInput />
            </Combobox>
          </Field>
        </FieldGroup>
      ))}
    </VStack>
  )
}
```

## Native validation

Use `validate` or native attributes such as `required`, then render `FieldError`.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Field, FieldLabel, FieldError, FieldValidity } from "@/components/ui/field"
import { Textarea } from "@/components/ui/textarea"

const styles = stylex.create({ field: { width: "20rem" } })

export default function FieldValidation() {
  return (
    <Field
      name="message"
      validationMode="onBlur"
      validate={(value) => (String(value).length < 20 ? "Write at least 20 characters." : null)}
      xstyle={styles.field}
    >
      <FieldLabel>Message</FieldLabel>
      <Textarea required placeholder="Type a message, then leave the field" />
      <FieldError />
      <FieldValidity>
        {({ validity }) => (validity.valid === true ? <output>Message ready.</output> : null)}
      </FieldValidity>
    </Field>
  )
}
```

## Accessibility

- `Fieldset` and `FieldsetLegend` keep related controls grouped for keyboard and
  assistive tech users.
- `FieldLabel` and `FieldDescription` associate automatically with the control.
- `FieldError` renders `role="alert"`, so a message that appears is announced.
- Apply `FieldSeparator` sparingly. Each one is a section boundary a screen
  reader stops at.

## Grouped choices

Wrap each checkbox or radio and its label in `FieldItem` to give it a separate label association within one field. See [Checkbox Group](https://carbon.style/docs/components/checkbox-group).

## API reference

Pass `render` to replace a part's element or compose it with another component.

`Field`, `FieldLabel`, `FieldDescription` and `FieldError` wrap the corresponding [Base UI Field parts](https://base-ui.com/react/components/field). Styled parts accept `xstyle`, `className`, inline `style` and `render`. Layout parts keep their native attributes.

### Field

| Prop          | Type                                         | Default      |
| ------------- | -------------------------------------------- | ------------ |
| `orientation` | `"vertical" \| "horizontal" \| "responsive"` | `"vertical"` |
| `invalid`     | `boolean`                                    | —            |
| `disabled`    | `boolean`                                    | `false`      |

`Field` passes `disabled` to the control and dims the label and title. It passes `invalid`, `name`, `validate`, `validationMode`, `dirty` and `touched` to Base UI.

See [Fieldset](https://carbon.style/docs/components/fieldset) for `Fieldset` and `FieldsetLegend`.

### FieldGroup

| Prop  | Type          | Default |
| ----- | ------------- | ------- |
| `gap` | `SpacingStep` | `5`     |

### FieldLabel

| Prop       | Type      | Default |
| ---------- | --------- | ------- |
| `checked`  | `boolean` | —       |
| `disabled` | `boolean` | —       |

`checked` draws the choice-card frame and only applies when the label wraps a
`Field`. `disabled` falls back to the enclosing field's value.

### FieldError

| Prop     | Type                                       | Default |
| -------- | ------------------------------------------ | ------- |
| `errors` | `Array<{ message?: string } \| undefined>` | —       |
| `match`  | `boolean \| keyof ValidityState`           | —       |

Children take precedence over `errors`. With neither, Base UI displays the validation message. Pass `match` to select a validity condition. `FieldValidity` exposes Base UI validity through a render function.

### fieldStyles

The style maps behind the component. Spread them onto any element.

| Key            | What it is                                                 |
| -------------- | ---------------------------------------------------------- |
| `field`        | The row or column: flex, full width, the gap between parts |
| `orientations` | `vertical`, `horizontal` and `responsive` direction maps   |
| `invalid`      | The destructive text colour                                |
| `control`      | Full width that becomes `auto` once a responsive row forms |
| `card`         | The choice-card panel: raised surface, radius, inset       |
| `description`  | Muted small text                                           |
| `error`        | Destructive small text                                     |

`card` is the panel alone. The component merges the selected edge and the focus
ring over it. The ring is `states.ring` from `theme/states.ts`, with no
pseudo-class on it. The card tracks focus itself, from a focus event that
bubbles up from the control inside it.

### FieldItem

`FieldItem` accepts Base UI `Field.Item` props, including `disabled` and `render`, plus `xstyle`, `className`, and inline `style`. It adds no layout styles.

### Data attributes

| Attribute                                                                                                  | On                 | Effect                                      |
| ---------------------------------------------------------------------------------------------------------- | ------------------ | ------------------------------------------- |
| `data-slot="field-item"`                                                                                   | individual choice  | Label and description scope                 |
| `data-slot="field"`                                                                                        | the field          | Targets the field from a parent             |
| `data-orientation`                                                                                         | the field          | Mirrors `orientation`                       |
| `data-valid`, `data-invalid`, `data-disabled`, `data-dirty`, `data-touched`, `data-filled`, `data-focused` | the field          | Base UI validity and disabled state         |
| `data-slot="field-group"`                                                                                  | the group          | The container a responsive field queries    |
| `data-slot="field-content"`                                                                                | the text column    |                                             |
| `data-slot="field-label"`                                                                                  | the label          | `data-checked` present on a selected card   |
| `data-slot="field-title"`                                                                                  | the title          |                                             |
| `data-slot="field-description"`                                                                            | the description    | Where `interop.css` styles a link inside it |
| `data-slot="field-separator"`                                                                              | the separator      | `data-content` present when it carries text |
| `data-slot="field-separator-line"`                                                                         | the rule           | Renders `role="separator"`                  |
| `data-slot="field-separator-content"`                                                                      | the text on a rule |                                             |
| `data-slot="field-error"`                                                                                  | the error          | Renders `role="alert"`                      |
