Field

Field helps structure form controls with consistent spacing, semantics, and validation messaging.

When to use

  • Group labels, controls, descriptions, and validation states into a single unit.
  • Build complex forms with consistent spacing and responsive alignment.
  • Display inline or grouped error messages with predictable styling.

Behavior notes

  • Use Field for each control row and FieldContent for control + helper text.
  • Add data-invalid='true' on Field to apply invalid-state visuals.
  • FieldError can render passed children or derive messages from an errors array.
  • Use FieldSet and FieldLegend to group related controls semantically.

Example

<Field>
  <FieldLabel htmlFor='email'>Email</FieldLabel>
  <FieldContent>
    <Input id='email' placeholder='name@company.com' />
    <FieldDescription>
      We use this for login and notifications.
    </FieldDescription>
    <FieldError errors={[{ message: 'Email is required' }]} />
  </FieldContent>
</Field>

Preview

This label appears in dashboards and reports.

Enabled

Email weekly product updates.

Billing cycle

Usage

import { Checkbox, Input, RadioGroup, RadioGroupItem } from '@branditdev/ui';

import {
  Field,
  FieldContent,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
  FieldSet,
  FieldTitle,
} from './field';

export function FieldDemo() {
  return (
    <FieldSet className='w-full max-w-md'>
      <FieldGroup>
        <Field>
          <FieldLabel htmlFor='name'>Team name</FieldLabel>
          <FieldContent>
            <Input id='name' defaultValue='Growth Platform' />
            <FieldDescription>
              This label appears in dashboards and reports.
            </FieldDescription>
          </FieldContent>
        </Field>

        <Field>
          <FieldLabel htmlFor='notifications'>Notifications</FieldLabel>
          <FieldContent>
            <div className='flex items-center gap-2'>
              <Checkbox id='notifications' defaultChecked />
              <span className='text-sm'>Enabled</span>
            </div>
            <FieldDescription>Email weekly product updates.</FieldDescription>
          </FieldContent>
        </Field>

        <Field>
          <FieldContent>
            <FieldTitle>Billing cycle</FieldTitle>
            <RadioGroup defaultValue='monthly'>
              <div className='flex items-center gap-2'>
                <RadioGroupItem id='cycle-monthly' value='monthly' />
                <FieldLabel htmlFor='cycle-monthly'>Monthly</FieldLabel>
              </div>
              <div className='flex items-center gap-2'>
                <RadioGroupItem id='cycle-annual' value='annual' />
                <FieldLabel htmlFor='cycle-annual'>Annual</FieldLabel>
              </div>
            </RadioGroup>
          </FieldContent>
        </Field>

        <Field data-invalid='true'>
          <FieldLabel htmlFor='api-key'>API key</FieldLabel>
          <FieldContent>
            <Input id='api-key' defaultValue='pk_live_1234' />
            <FieldError errors={[{ message: 'API key must start with sk_' }]} />
          </FieldContent>
        </Field>
      </FieldGroup>
    </FieldSet>
  );
}

API

FieldSet(props): Element

Defined in: components/field/field.tsx:15

Field UI component.

Parameters

NameTypeOptionalDefaultDescription
propsDetailedHTMLProps<FieldsetHTMLAttributes<HTMLFieldSetElement>>no-Component properties.

Returns

Element

Rendered component.

Source

import { cva, type VariantProps } from 'class-variance-authority';
import { useMemo } from 'react';

import { cn, Label, Separator } from '@branditdev/ui';

export 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}
    />
  );
}

export 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}
    />
  );
}

export 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 [&>.sr-only]:w-auto'],
        horizontal: [
          'flex-row items-center',
          '[&>[data-slot=field-label]]:flex-auto',
          'has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
        ],
        responsive: [
          'flex-col @md/field-group:flex-row @md/field-group:items-center [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto',
          '@md/field-group:[&>[data-slot=field-label]]:flex-auto',
          '@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
        ],
      },
    },
    defaultVariants: {
      orientation: 'vertical',
    },
  }
);

export 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}
    />
  );
}

export 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}
    />
  );
}

export function FieldLabel({
  className,
  ...props
}: React.ComponentProps<typeof Label>) {
  return (
    <Label
      data-slot='field-label'
      className={cn(
        'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
        'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border *:data-[slot=field]:p-4',
        'has-data-[state=checked]:border-primary has-data-[state=checked]:bg-primary/5 dark:has-data-[state=checked]:bg-primary/10',
        className
      )}
      {...props}
    />
  );
}

export function FieldTitle({
  className,
  ...props
}: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot='field-label'
      className={cn(
        'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
        className
      )}
      {...props}
    />
  );
}

export function FieldDescription({
  className,
  ...props
}: React.ComponentProps<'p'>) {
  return (
    <p
      data-slot='field-description'
      className={cn(
        'text-sm leading-normal font-normal text-muted-foreground group-has-data-[orientation=horizontal]/field:text-balance',
        'last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5',
        '[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
        className
      )}
      {...props}
    />
  );
}

export function FieldSeparator({
  children,
  className,
  ...props
}: React.ComponentProps<'div'> & {
  children?: React.ReactNode;
}) {
  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
          className='relative mx-auto block w-fit bg-background px-2 text-muted-foreground'
          data-slot='field-separator-content'
        >
          {children}
        </span>
      )}
    </div>
  );
}

export function FieldError({
  className,
  children,
  errors,
  ...props
}: React.ComponentProps<'div'> & {
  errors?: Array<{ message?: string } | undefined>;
}) {
  const content = useMemo(() => {
    if (children) {
      return children;
    }

    if (!errors?.length) {
      return null;
    }

    const uniqueErrors = [
      ...new Map(errors.map(error => [error?.message, error])).values(),
    ];

    if (uniqueErrors?.length == 1) {
      return uniqueErrors[0]?.message;
    }

    return (
      <ul className='ml-4 flex list-disc flex-col gap-1'>
        {uniqueErrors.map(
          (error, index) =>
            error?.message && <li key={index}>{error.message}</li>
        )}
      </ul>
    );
  }, [children, errors]);

  if (!content) {
    return null;
  }

  return (
    <div
      role='alert'
      data-slot='field-error'
      className={cn('text-sm font-normal text-destructive', className)}
      {...props}
    >
      {content}
    </div>
  );
}