# Text field

A text input cut into the panel as a slot, with a printed label above and description and error lines below. Focus lights the slot's edge amber, the colour of a caret; an invalid field edges in red and lights a lamp by its label. Inputs, textareas, search boxes with an icon and a key hint, amounts with a unit.

- Category: Essentials
- Docs: https://bevel-ui.vercel.app/components/text-field
- Import from: `@/components/ui/text-field`

## Install

```bash
npx shadcn@latest add @bevel/text-field
```

npm dependencies (installed by the CLI): `@base-ui/react`, `cn`.

## Anatomy

```tsx
<TextField name="email">
  <TextFieldLabel />
  <TextFieldSlot>
    <TextFieldAdornment />
    <TextFieldInput />
    <TextFieldKbd />
  </TextFieldSlot>
  <TextFieldDescription />
  <TextFieldError />
</TextField>
```

## Parts

| Part | Renders | Description |
| --- | --- | --- |
| `TextField` | Field.Root | size sm or md. Everything Base UI Field takes: name, disabled, invalid, validate, validationMode. Wires the label, description and error to the control. |
| `TextFieldLabel` | Field.Label | The printed label. Its lamp lights red while the field is invalid. |
| `TextFieldSlot` | div | The slot, when there are adornments. Pressing anywhere in it focuses the control. Optional: a bare TextFieldInput is its own slot. |
| `TextFieldInput` | Input | The input. value, defaultValue, onValueChange, and every input attribute. |
| `TextFieldTextarea` | Field.Control (textarea) | Several lines, resizable vertically. |
| `TextFieldAdornment` | span | An icon, unit or prefix printed in the slot. |
| `TextFieldKbd` | kbd | A small keycap: the shortcut that focuses the field. |
| `TextFieldDescription` | Field.Description | What goes in it. Describes the control. |
| `TextFieldError` | Field.Error (alert) | Shown while invalid and announced as it appears. match a validity state (valueMissing, typeMismatch…) or leave it off to show validate's message or the form's. |

## Example: Workspace settings

Fields validate as you leave them and again on save; the server's answer comes back on the field it's about. Save as is: the URL is taken.

```tsx
"use client"

import * as React from "react"
import { Form } from "@base-ui/react/form"
import {
  TextField,
  TextFieldAdornment,
  TextFieldDescription,
  TextFieldError,
  TextFieldInput,
  TextFieldLabel,
  TextFieldSlot,
} from "@/components/ui/text-field"
import { ToggleLever, ToggleLeverDescription, ToggleLeverField, ToggleLeverLabel } from "@/components/ui/toggle-lever"
import { LatchCheckbox, LatchCheckboxField, LatchCheckboxLabel } from "@/components/ui/latch-checkbox"

/**
 * Workspace settings. Fields validate when you leave them and again on save;
 * the server's answer comes back as an error on the field it's about.
 */
export function TextFieldSettings() {
  const [errors, setErrors] = React.useState<Record<string, string>>({})
  const [saved, setSaved] = React.useState<string | null>(null)

  return (
    <Form
      errors={errors}
      validationMode="onBlur"
      onFormSubmit={(values) => {
        setSaved(null)
        // Pretend the server owns the slug namespace.
        if (values.slug === "acme") {
          setErrors({ slug: "acme is taken. Try acme-labs." })
          return
        }
        setErrors({})
        setSaved(`Saved ${values.name}.`)
      }}
      className="flex w-full flex-col gap-5 rounded-xl border bg-card p-5"
    >
      <TextField name="name">
        <TextFieldLabel>Workspace name</TextFieldLabel>
        <TextFieldInput required defaultValue="Acme" autoComplete="organization" />
        <TextFieldError match="valueMissing">Give the workspace a name.</TextFieldError>
      </TextField>

      <TextField
        name="slug"
        validate={(value) =>
          !value ? "Choose a URL." : /^[a-z0-9-]+$/.test(String(value)) ? null : "Only lowercase letters, numbers and dashes."
        }
      >
        <TextFieldLabel>URL</TextFieldLabel>
        <TextFieldSlot>
          <TextFieldAdornment>bevel.app/</TextFieldAdornment>
          <TextFieldInput defaultValue="acme" spellCheck={false} autoCapitalize="none" />
        </TextFieldSlot>
        <TextFieldDescription>Lowercase letters, numbers and dashes. Try saving as is.</TextFieldDescription>
        {/* Shows the validate message, or the server's. */}
        <TextFieldError />
      </TextField>

      <TextField name="billing">
        <TextFieldLabel>Billing email</TextFieldLabel>
        <TextFieldInput type="email" required placeholder="finance@acme.com" autoComplete="email" />
        <TextFieldError match="valueMissing">Invoices need somewhere to go.</TextFieldError>
        <TextFieldError match="typeMismatch">That isn't a whole email address.</TextFieldError>
      </TextField>

      <div className="flex flex-col gap-4 border-t pt-4">
        <ToggleLeverField name="digest">
          <ToggleLeverLabel>Weekly digest</ToggleLeverLabel>
          <ToggleLeverDescription>Runs, spend and failures, every Monday.</ToggleLeverDescription>
          <ToggleLever size="sm" defaultChecked />
        </ToggleLeverField>
        <LatchCheckboxField name="capAlert">
          <LatchCheckbox defaultChecked />
          <LatchCheckboxLabel>Email me when spend passes the monthly cap</LatchCheckboxLabel>
        </LatchCheckboxField>
      </div>

      <div className="flex items-center justify-between gap-3">
        <p role="status" className="text-sm text-muted-foreground">
          {saved}
        </p>
        <button
          type="submit"
          className="h-9 rounded-md bg-foreground px-4 text-sm font-medium text-background outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
        >
          Save
        </button>
      </div>
    </Form>
  )
}
```

## Example: Search box

An icon and a key hint in the slot. ⌘K or / focuses it from anywhere on the page.

```tsx
"use client"

import * as React from "react"
import { Search } from "lucide-react"
import {
  TextField,
  TextFieldAdornment,
  TextFieldInput,
  TextFieldKbd,
  TextFieldLabel,
  TextFieldSlot,
} from "@/components/ui/text-field"

const items = ["Text field", "Toggle lever", "Latch checkbox", "Preset keys", "Wallet", "Meter", "DIP switches"]

/** A site search box: ⌘K or / focuses it from anywhere on the page. */
export function TextFieldSearch() {
  const [query, setQuery] = React.useState("")
  const ref = React.useRef<HTMLInputElement>(null)

  React.useEffect(() => {
    const onKeyDown = (event: KeyboardEvent) => {
      const typing = (event.target as HTMLElement).closest("input, textarea, [contenteditable]")
      if ((event.key === "k" && (event.metaKey || event.ctrlKey)) || (event.key === "/" && !typing)) {
        event.preventDefault()
        ref.current?.focus()
      }
    }
    window.addEventListener("keydown", onKeyDown)
    return () => window.removeEventListener("keydown", onKeyDown)
  }, [])

  const matches = items.filter((item) => item.toLowerCase().includes(query.trim().toLowerCase()))

  return (
    <div className="flex w-full flex-col gap-3">
      <TextField size="sm">
        <TextFieldLabel className="sr-only">Search components</TextFieldLabel>
        <TextFieldSlot>
          <TextFieldAdornment>
            <Search aria-hidden />
          </TextFieldAdornment>
          <TextFieldInput
            ref={ref}
            type="search"
            placeholder="Search components"
            value={query}
            onValueChange={setQuery}
            aria-keyshortcuts="Meta+K /"
          />
          <TextFieldKbd>⌘K</TextFieldKbd>
        </TextFieldSlot>
      </TextField>
      <ul className="flex flex-col gap-1 text-sm" aria-live="polite">
        {matches.length ? (
          matches.map((item) => (
            <li key={item} className="rounded-md px-2 py-1 text-muted-foreground">
              {item}
            </li>
          ))
        ) : (
          <li className="px-2 py-1 text-muted-foreground">Nothing matches “{query}”.</li>
        )}
      </ul>
    </div>
  )
}
```
