Skip to content
Wave 1

Field

Groups a label, control, description and error, and wires their ARIA relationships.

Install

Terminal
pnpm dlx shadcn@latest add @acme/field

Or import from the package: import { Field } from '@acme/react'

Examples

Validation

At least 3 characters. Letters, numbers and dashes.

field-validation.tsx
'use client'

import { Form } from '@base-ui/react/form'
import { Button } from '@/components/ui/button'
import { Field, FieldDescription, FieldError, FieldLabel } from '@/components/ui/field'
import { Input } from '@/components/ui/input'

function validateUsername(value: unknown) {
  const v = String(value ?? '')
  if (v && v.length < 3) return 'Use at least 3 characters.'
  if (v && !/^[a-z0-9-]+$/i.test(v)) return 'Use only letters, numbers and dashes.'
  return null
}

/** Validation runs on blur and on submit; errors are linked to the input and announced. */
export default function FieldValidation() {
  return (
    <Form className="flex max-w-sm flex-col gap-6" onSubmit={(e) => e.preventDefault()}>
      <Field validationMode="onBlur" validate={validateUsername}>
        <FieldLabel>Username</FieldLabel>
        <Input name="username" autoComplete="username" required />
        <FieldDescription>At least 3 characters. Letters, numbers and dashes.</FieldDescription>
        <FieldError match="valueMissing">Enter a username.</FieldError>
        <FieldError />
      </Field>
      <div>
        <Button type="submit">Create account</Button>
      </div>
    </Form>
  )
}

Basic

We send invoices here.

input-basic.tsx
import { Field, FieldDescription, FieldError, FieldLabel } from '@/components/ui/field'
import { Input } from '@/components/ui/input'

export default function InputBasic() {
  return (
    <div className="flex max-w-sm flex-col gap-6">
      <Field>
        <FieldLabel>Work email</FieldLabel>
        <Input
          type="email"
          name="email"
          autoComplete="email"
          placeholder="name@company.com"
          required
        />
        <FieldDescription>We send invoices here.</FieldDescription>
        <FieldError match="valueMissing">Enter your work email.</FieldError>
        <FieldError match="typeMismatch">Enter an email like name@company.com.</FieldError>
      </Field>
      <Field disabled>
        <FieldLabel>Account ID</FieldLabel>
        <Input defaultValue="acct_4821" />
      </Field>
    </div>
  )
}

Native

The same component for iOS (SwiftUI, AcmeUI) and Android (Jetpack Compose, com.acme.ui), built on each platform's own controls with the same tokens.

AcmeTextField / AcmeSelect (description:, error:, isRequired:)

Swift
AcmeTextField("Card number", text: $card, error: "Enter all 16 digits of the card number.")
  • The error replaces the description, shows an icon, and becomes the VoiceOver hint.
Field on iOS, light mode
iOS, light mode
Field on iOS, dark mode
iOS, dark mode

Props

Field props
PropTypeDefaultDescription
validationMode'onSubmit' | 'onBlur' | 'onChange''onSubmit'When to validate.
invalidboolean—Force the invalid state, e.g. from a server error or a form library.
disabledboolean—Disables the label and control together.
FieldError.matchboolean | ValidityState key—Which validity state shows this error (valueMissing, typeMismatch, tooShort…). true always shows it.

Accessibility

You provide
  • Exactly one FieldLabel per Field.
Built in
  • Description and error are linked with aria-describedby; the control gets aria-invalid.
  • FieldError shows an icon plus text, never color alone.
WCAG 2.2 criteria
1.3.1, 1.4.1, 3.3.1, 3.3.2, 3.3.3, 4.1.2

Don’t

  • Show errors only as a red border.
  • Validate on every keystroke for fields people are still typing.

Tokens

  • color.muted-foreground
  • color.destructive