Field

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

Payment Method

All transactions are secure and encrypted

Enter your 16-digit card number

Billing Address

The billing address associated with your payment method

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

Install the following dependencies:

pnpm add @base-ui/react @stylexjs/stylex clsx

Set up the token layer and the StyleX compiler, if you have not already. See 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, Separator and 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.

Copy and paste the following code into your project.

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,
}

Update the import paths to match your project setup.

Use Fieldset to group related fields.

Usage

import {
  Field,
  FieldContent,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
  FieldSeparator,
  FieldTitle,
} from "@/components/ui/field"
import { FieldsetLegend, Fieldset } from "@/components/ui/fieldset"
<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.

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

FieldGroup

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

FieldGroup
├── Field
├── FieldSeparator
└── Field

Fieldset

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

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

Anatomy

<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 for building a form with Field, schema validation and per-field errors.

Input

Choose a unique username for your account.

Must be at least 8 characters long.

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.

Address Information

We need your address to deliver your order.

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.

Show these items on the desktop

Select the items you want to show on the desktop.

Your Desktop & Documents folders are being synced with iCloud Drive. You can access them from other devices.

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.

Compute Environment

Select the compute environments for your cluster.

"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>
  )
}

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.

Responses

Get notified when ChatGPT responds to requests that take time, like research or image generation.

Tasks

Get notified when tasks you've created have updates. Manage tasks

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

Fill in your profile information.

Provide your full name for identification

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>
  )
}

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.

We only use it to send your receipt.

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>
  )
}
<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.

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

Inherited state

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

Invalid

https://

Disabled and invalid

https://
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.

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.

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. Styled parts accept xstyle, className, inline style and render. Layout parts keep their native attributes.

Field

PropTypeDefault
orientation"vertical" | "horizontal" | "responsive""vertical"
invalidboolean—
disabledbooleanfalse

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 for Fieldset and FieldsetLegend.

FieldGroup

PropTypeDefault
gapSpacingStep5

FieldLabel

PropTypeDefault
checkedboolean—
disabledboolean—

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

PropTypeDefault
errorsArray<{ message?: string } | undefined>—
matchboolean | 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.

KeyWhat it is
fieldThe row or column: flex, full width, the gap between parts
orientationsvertical, horizontal and responsive direction maps
invalidThe destructive text colour
controlFull width that becomes auto once a responsive row forms
cardThe choice-card panel: raised surface, radius, inset
descriptionMuted small text
errorDestructive 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

AttributeOnEffect
data-slot="field-item"individual choiceLabel and description scope
data-slot="field"the fieldTargets the field from a parent
data-orientationthe fieldMirrors orientation
data-valid, data-invalid, data-disabled, data-dirty, data-touched, data-filled, data-focusedthe fieldBase UI validity and disabled state
data-slot="field-group"the groupThe container a responsive field queries
data-slot="field-content"the text column
data-slot="field-label"the labeldata-checked present on a selected card
data-slot="field-title"the title
data-slot="field-description"the descriptionWhere interop.css styles a link inside it
data-slot="field-separator"the separatordata-content present when it carries text
data-slot="field-separator-line"the ruleRenders role="separator"
data-slot="field-separator-content"the text on a rule
data-slot="field-error"the errorRenders role="alert"