# Accessibility (/ui/docs/react/accessibility)
Accessibility is built in, not bolted on. Appica UI wraps [Base UI](https://base-ui.com)
primitives, which implement the WAI-ARIA patterns — keyboard interaction, focus
management, and ARIA wiring — so the hard parts are handled. This page covers
what you get for free and the few things that remain your responsibility.
## What's handled for you [#whats-handled-for-you]
* **Keyboard interaction** — menus, listboxes, tabs, sliders, and dialogs
implement the expected arrow-key, `Home`/`End`, `Escape`, and typeahead
behavior, including roving `tabindex` within groups.
* **Focus management** — overlays (Dialog, Drawer, Popover) trap focus while open
and restore it to the trigger on close.
* **ARIA wiring** — roles, `aria-expanded`, `aria-controls`, `aria-selected`, and
label/description associations are set on the right elements.
* **Reduced motion** — every animation is reduced-motion aware (via
`motion-reduce:transition-none` or `motion-safe:`), so motion is skipped for
users who ask for it. See [Animation](/ui/docs/react/animation).
## Focus visibility [#focus-visibility]
A focus ring is applied globally on `:focus-visible` (keyboard focus), using the
ring tokens from [Colors](/ui/docs/react/colors#focus-rings). It stays out of the way for mouse
users and appears for keyboard users. Keep it intact — don't strip outlines from
interactive elements.
## Labeling controls [#labeling-controls]
Associate a visible label with a single control using `Field` and `FieldLabel`,
which wire up the `for`/`id` relationship automatically:
```tsx
import { Field, FieldLabel } from '@appica/ui-react/field'
import { Input } from '@appica/ui-react/input'
export default function EmailField() {
return (
Email
)
}
```
`FieldLabel` renders a `
```tsx
LeftCenter
```
Controls with no visible label (an icon-only button, a search field) still need an
accessible name — give them `aria-label`:
```tsx
```
## Color contrast [#color-contrast]
The default light and dark palettes are tuned for legible contrast. If you
[recolor the system](/ui/docs/react/theming), re-check your accent and foreground
pairings against [WCAG](https://www.w3.org/WAI/WCAG22/quickref/) — especially the
`*-foreground` token that sits on a colored fill.
## Internationalization [#internationalization]
For right-to-left languages, mirror the UI with
[`DirectionProvider`](/ui/docs/react/rtl) so keyboard navigation and layout follow
the reading direction.
## Verify your own usage [#verify-your-own-usage]
The components are tested with `vitest-axe`, but composition is where issues
creep in. Audit your screens with the same tooling (axe, Lighthouse) and test
keyboard-only flows — the components give you a correct foundation, not a
guarantee that every assembled page is accessible.
# Animation (/ui/docs/react/animation)
Components in Appica UI animate their open, close, and state changes out of the
box. Most of this motion is plain CSS, but a few components use
[Motion](https://motion.dev) when an interaction needs smoother, interruptible
animation. In both cases, if the user has asked their system to reduce motion,
the animations are turned off automatically — see [Reduced motion](#reduced-motion).
## State-driven transitions [#state-driven-transitions]
Interactive components expose their state as `data-*` attributes — `data-open`,
`data-closed`, `data-starting-style`, `data-ending-style`, and so on. Transitions
hang off those attributes through Tailwind's `data-*` variants, so enter and exit
animations are just utility classes — no animation library required:
```tsx
// The pattern components use internally
```
The element starts and ends scaled-down and transparent, and animates to its
natural state while open. Because exit is just another state
(`data-ending-style`), elements animate out before they unmount. The single
`motion-reduce:transition-none` is what drops the animation when the user prefers
reduced motion — see below.
## Reduced motion [#reduced-motion]
Every animated component is reduced-motion aware. Most disable their transition
in one line with `motion-reduce:transition-none`; a few instead gate each rule
behind `motion-safe:`. Both rely on the same pair of custom variants the library
defines:
```css
@custom-variant motion-reduce {
@media (prefers-reduced-motion: reduce) {
@slot;
}
&:is([data-disable-animations] *) {
@slot;
}
}
@custom-variant motion-safe {
@media (prefers-reduced-motion: no-preference) {
&:not([data-disable-animations] *) {
@slot;
}
}
}
```
This means animations are skipped in two situations: when the OS requests reduced
motion, **or** when a [`ReducedMotionProvider`](/ui/docs/react/reduced-motion-provider)
sets `data-disable-animations` — which also reaches portaled popups, since they
render outside the React tree.
Use the same variants in your own markup so it honors the preference too:
```tsx
// One-line opt-out — the common pattern
…
// Or gate each rule with motion-safe:
…
```
## Disabling animations globally [#disabling-animations-globally]
Wrap a subtree (or the whole app) to force-disable animation regardless of the OS
setting — useful for tests, demos, or a user preference toggle:
```tsx
import { ReducedMotionProvider } from '@appica/ui-react/providers/reduced-motion-provider'
export default function Providers({ children }) {
return {children}
}
```
For JavaScript-driven animation (e.g. with Motion), read the resolved preference
with [`useReducedMotion`](/ui/docs/react/use-reduced-motion):
```tsx
'use client'
import { useReducedMotion } from '@appica/ui-react/hooks/use-reduced-motion'
function Reveal() {
const reduced = useReducedMotion()
return
…
}
```
## Customizing transitions [#customizing-transitions]
Tune timing per instance by overriding the relevant utilities through `className`
— tailwind-merge keeps the last value:
```tsx
```
For larger durations, easing, and keyframes, override the corresponding tokens in
your stylesheet — see [Theming](/ui/docs/react/theming).
# Colors (/ui/docs/react/colors)
Appica UI's palette is organized by **role**, not by hue. Instead of `blue-500`,
you use `primary` or `info-emphasis`. This keeps components meaningful and lets a
single token override retheme the whole system. Every color is defined in
[oklch](https://oklch.com) and is overridable — see [Theming](/ui/docs/react/theming).
Each color is exposed as a Tailwind utility (`bg-*`, `text-*`, `border-*`) and as
a raw CSS variable named after the role — `var(--primary)`, `var(--foreground)`,
and so on. See [Theming](/ui/docs/react/theming#using-tokens-in-your-own-ui).
## Base colors [#base-colors]
The grayscale foundation of every layout: backgrounds, text, and borders. Each family
is a scale from subtle to intense.
### Foreground (text & icons) [#foreground-text--icons]
| Token | Use for |
| --------------------- | -------------------------------------- |
| `foreground` | Default body text |
| `foreground-muted` | Secondary text, descriptions |
| `foreground-subtle` | Placeholders, disabled labels, markers |
| `foreground-strong` | Emphasized text |
| `foreground-emphasis` | Stronger emphasis |
| `foreground-intense` | Headings, highest-contrast text |
| `foreground-inverse` | Text on an inverted surface |
### Background (surfaces) [#background-surfaces]
| Token | Use for |
| -------------------- | --------------------------------- |
| `background` | The base page surface |
| `background-subtle` | Slightly raised areas, cards |
| `background-muted` | Hover fills, inset panels |
| `background-strong` | More prominent fills |
| `background-inverse` | Inverted surfaces (e.g. tooltips) |
### Border [#border]
| Token | Use for |
| ----------------- | ---------------------------- |
| `border` | Default borders and dividers |
| `border-muted` | Faint separators |
| `border-strong` | Higher-contrast borders |
| `border-emphasis` | Emphasized outlines |
| `border-intense` | Strongest outlines |
| `border-inverse` | Borders on inverted surfaces |
## Accent palettes [#accent-palettes]
Six semantic accents share an identical scale, so they're interchangeable by
intent. `primary` and `secondary` carry your brand; `error`, `success`,
`warning`, and `info` carry status.
| Step | Role |
| -------------- | ----------------------------------------------- |
| `*-subtle` | Translucent tint — soft background wash |
| `*-soft` | Slightly stronger translucent fill |
| `*-muted` | Muted solid |
| `*` (base) | The default accent fill |
| `*-strong` | Stronger, for hover/active |
| `*-emphasis` | The vivid, on-brand value |
| `*-intense` | The boldest value |
| `*-foreground` | Text/icon color that sits legibly on the accent |
For example, an info alert pairs `bg-info-subtle` (surface), `bg-info-soft` (border), with `text-info-foreground`;
a solid primary button pairs `bg-primary` with `text-primary-foreground`.
## Focus rings [#focus-rings]
Focus styling has its own token set so the ring can differ from borders. Each maps
to a matching role, and components apply them automatically on `:focus-visible` —
see [Accessibility](/ui/docs/react/accessibility).
| Token | Use for |
| ---------------- | ------------------------------------ |
| `ring` | Default focus ring, neutral controls |
| `ring-input` | Inputs and form fields |
| `ring-primary` | Primary controls |
| `ring-secondary` | Secondary controls |
| `ring-error` | Error / invalid controls |
| `ring-success` | Success controls |
| `ring-warning` | Warning controls |
| `ring-info` | Info controls |
| `ring-light` | Controls on inverted / dark surfaces |
## Overriding the palette [#overriding-the-palette]
Any token can be redefined after the stylesheet import, separately per theme:
```css
:root {
--secondary: oklch(0.827 0.119 306.383);
}
.dark {
--secondary: oklch(0.714 0.203 305.504);
}
```
Head to [Theming](/ui/docs/react/theming) for the raw-vs-theme token model and the
non-color tokens (radius, shadows, typography).
# Composition (/ui/docs/react/composition)
Appica UI components ship fully styled, but you often need them to *be* something
else — a navigation link that's actually your router's `Link`, a tooltip trigger
that's actually your own button. The `render` prop, inherited from
[Base UI](https://base-ui.com), makes that possible without losing the
component's styling or behavior.
For the related case of a plain link that should merely *look* like a button, use
the exported variant helpers instead — see [Links that look like a button](#links-that-look-like-a-button).
## Rendering as a different element [#rendering-as-a-different-element]
Pass an element to `render` and the component adopts it as its output, merging in
its own props, classes, and event handlers. For instance, render a `Badge` as an
`` so a status chip becomes a real link:
```tsx
import { Badge } from '@appica/ui-react/badge'
// Renders an styled as a badge.
export default function TagBadge() {
return }>React
}
```
This is safe because `Badge` adds no interactive semantics. `Button` is different — it *enforces* button
semantics (`role`, keyboard handling), so rendering an `` through it produces a link masquerading as a
button, which misleads assistive tech. To make a link merely *look* like a button, style the `` directly —
see [Links that look like a button](#links-that-look-like-a-button).
## Rendering as a component [#rendering-as-a-component]
`render` also accepts a component element — ideal for framework routers. A
`NavigationLink` stays a link while delegating navigation to your router's `Link`:
```tsx
import Link from 'next/link'
import { NavigationLink } from '@appica/ui-react/navigation'
export default function PricingLink() {
return }>Pricing
}
```
The same pattern composes the interactive parts of larger components. For
instance, make a tooltip's trigger your own button:
```tsx
import { Tooltip, TooltipTrigger, TooltipContent } from '@appica/ui-react/tooltip'
import { Button } from '@appica/ui-react/button'
export default function HintedButton() {
return (
Hover me} />
Helpful hint
)
}
```
## Links that look like a button [#links-that-look-like-a-button]
Sometimes you don't want to *change* a component's element — you want a plain link
(or other element) to simply wear a component's styling. Don't reach for `render`
here: rendering an `` through `Button` forces button semantics onto a link.
Instead, apply the exported `buttonVariants` classes directly to the element that
already has the right semantics:
```tsx
import { buttonVariants } from '@appica/ui-react/button'
// A real link that looks like a primary button.
Read the docs
// Works with a router Link too.
View pricing
```
Links and buttons have different, non-interchangeable semantics: a link navigates, a button performs an
action. The `render` prop is for adopting a *different element while keeping the component's role* — not for
repainting a link as a button. Styling the `` with `buttonVariants` keeps link semantics intact and gives
you the identical look.
`buttonVariants` isn't the only one — several components export their variant
recipe for exactly this purpose:
| Helper | Import from | Style any element like |
| ------------------------ | ----------------------------- | ---------------------------------- |
| `buttonVariants` | `@appica/ui-react/button` | Button (`variant`, `size`) |
| `inputVariants` | `@appica/ui-react/input` | Input (`variant`, `size`, `state`) |
| `navigationLinkVariants` | `@appica/ui-react/navigation` | NavigationLink (`variant`, `size`) |
Each takes the same variant props as its component and returns a className string,
so they drop into any element and compose with your own classes through `cn` or a
template literal.
## Where to put `className` [#where-to-put-classname]
This is the one rule worth memorizing. When composing a render-prop wrapper:
* **Visual overrides** (`className`, `style`) go on the **wrapper**.
* **Structural/behavioral props** (`href`, `type`, `variant`, `size`, rendering as a different element) go on the JSX inside `render`.
```tsx
// ✅ Do — className on the wrapper
Help}
/>
// ❌ Don't — className on the render JSX
Help} />
```
Putting `className` on the inner JSX can cause a **React hydration mismatch** in Server Components: the styled element
is built on the server, serialized across the boundary, then cloned on the client, and the final class strings can
disagree. Keeping `className` on the wrapper avoids it — and class precedence still works, since the wrapper's classes
are merged last and win on conflicts.
If you specifically need a class on the inner element, extract the trigger into
its own `'use client'` component rather than promoting the whole page to client.
## Class merging [#class-merging]
Overrides are merged with [tailwind-merge](https://github.com/dcastil/tailwind-merge),
so conflicting utilities are de-duplicated and the last one wins. You don't need
to fight specificity:
```tsx
// `rounded-full` overrides the component's default radius cleanly.
```
# Dark Mode (/ui/docs/react/dark-mode)
Appica UI is dark-mode-ready out of the box. The token system ships both a light
and a dark palette; switching themes is a matter of toggling a class on the
`` element, which [`ThemeProvider`](/ui/docs/react/theme-provider) manages for
you — including persistence and a no-flash first paint.
## How it works [#how-it-works]
Dark mode is **class-based**. When the `dark` class is on a `` ancestor,
the dark token values apply. The library defines the variant as:
```css
@custom-variant dark (&:is(.dark *));
```
So you can write dark-only utilities in your own markup too:
```tsx
…
```
## Setup [#setup]
Wrap your app in `ThemeProvider` at your app's root (covered in
[Installation](/ui/docs/react/installation)). It applies the correct class before
the page paints, so there's no flash of the wrong theme on load. The example
uses Next.js's root layout — see the
[Framework notes](/ui/docs/react/installation#framework-notes) for where this file
lives in Vite, React Router / Remix, TanStack Start, and Astro:
```tsx title="app/layout.tsx (Next.js)"
import { ThemeProvider } from '@appica/ui-react/providers/theme-provider'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
)
}
```
`suppressHydrationWarning` on `` is expected: the no-flash script sets the theme class before React
hydrates, so the server and client markup intentionally differ by that one attribute.
By default `ThemeProvider` follows the OS preference (`enableSystem`) and persists
the user's choice under the `theme` key in `localStorage`.
## Building a theme toggle [#building-a-theme-toggle]
Read and set the theme with [`useTheme`](/ui/docs/react/use-theme). Guard
theme-dependent UI with `mounted` so the server-rendered output matches the first
client render:
```tsx title="theme-toggle.tsx"
'use client'
import { useTheme } from '@appica/ui-react/hooks/use-theme'
import { Button } from '@appica/ui-react/button'
import { SunHigh, MoonStars } from '@appica/icons-react'
export function ThemeToggle() {
const { resolvedTheme, setTheme, mounted } = useTheme()
// Avoid rendering theme-specific content until mounted.
if (!mounted) return
const next = resolvedTheme === 'dark' ? 'light' : 'dark'
return (
)
}
```
* `theme` is the stored choice, which may be `'system'`.
* `resolvedTheme` is what's actually applied — `'system'` resolved to `'light'` or `'dark'`.
* `systemTheme` is the current OS preference.
## Common options [#common-options]
`ThemeProvider` accepts several props to tune behavior:
| Prop | Default | Description |
| --------------------------- | ------------------------------ | ------------------------------------------------- |
| `defaultTheme` | `'system'` (if `enableSystem`) | Theme used before the user makes a choice |
| `enableSystem` | `true` | Follow the OS `prefers-color-scheme` |
| `storageKey` | `'theme'` | `localStorage` key for the persisted choice |
| `forcedTheme` | — | Pin a theme for a subtree (e.g. a marketing page) |
| `disableTransitionOnChange` | `false` | Suppress CSS transitions during the switch |
| `enableColorScheme` | `true` | Set `color-scheme` so native controls match |
To force a single theme on a specific route, nest another provider with
`forcedTheme`:
```tsx
```
See [`ThemeProvider`](/ui/docs/react/theme-provider) for the full prop reference and
[`useTheme`](/ui/docs/react/use-theme) for the hook.
# Direction Provider (/ui/docs/react/direction-provider)
`DirectionProvider` sets the reading direction for descendant components. It's a
thin wrapper over [Base UI](https://base-ui.com)'s direction provider, so your app
depends on `@appica/ui-react` rather than the underlying primitive.
```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'
```
## Usage [#usage]
Wrap your app at its root and set `dir`. For full RTL support, also set the `dir`
attribute on `` so CSS logical properties and native text flow resolve
correctly — see [Right-to-Left (RTL)](/ui/docs/react/rtl). The example uses Next.js's
root layout; for the equivalent root file in other frameworks, see the
[Framework notes](/ui/docs/react/installation#framework-notes) in Installation.
```tsx title="app/layout.tsx (Next.js)"
{children}
```
Descendant components read the value via [`useDirection`](/ui/docs/react/use-direction).
Components that position elements with JavaScript — Slider, Switch, Carousel, and
overlays like menus — rely on it to know which side is "end."
Optional. With no provider, components default to `ltr`. Reach for it only when supporting RTL.
## Props [#props]
| Prop | Type | Default | Description |
| ---------- | ---------------- | ------- | ------------------------------------------- |
| `dir` | `'ltr' \| 'rtl'` | `'ltr'` | Reading direction for descendant components |
| `children` | `ReactNode` | — | The subtree that should read this direction |
# Fonts (/ui/docs/react/fonts)
Typography in Appica UI is driven by two design tokens — `--font-sans` and
`--font-mono` — exactly like colors and radii. Switching fonts is a two-part job:
**load** the font in your app, then **point the token at it**. The first part is
framework-specific; the second is one line of CSS that's the same everywhere.
## The default fonts [#the-default-fonts]
Out of the box, `@appica/ui-react/styles.css` sets both tokens to native system
font stacks:
```css
:root {
--font-sans:
ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
}
```
System fonts download nothing, render instantly, and never cause layout shift — a
good default. They feed Tailwind's `font-sans` / `font-mono` utilities through the
`@theme inline` block, and Tailwind's preflight applies `--font-sans` as the base
family for the whole document. So **overriding the raw `--font-sans` token re-fonts
your entire UI** — every component included.
Override the raw `--font-sans` token at `:root`, not its alias inside the `@theme inline` block. This mirrors how
[Theming](/ui/docs/react/theming) treats colors and radii — the theme layer is derived, so the raw token is the one to
redefine.
## The one line that always applies [#the-one-line-that-always-applies]
Whatever loads the font, you finish by redefining the raw token after the import,
keeping a fallback stack so text stays readable while the web font loads:
```css title="globals.css"
@import 'tailwindcss';
@import '@appica/ui-react/styles.css';
@source '@appica/ui-react';
:root {
--font-sans: 'Geist', ui-sans-serif, system-ui, sans-serif;
}
```
The font name on the left of the fallback list must match whatever the loading step
below makes available — a `font-family` from `@font-face`, an `@fontsource` package,
or the CSS variable `next/font` generates. The rest of this guide is just the
different ways to get that name onto the page.
## Loading the font [#loading-the-font]
How you load a font depends on your framework. Next.js has a first-class font
pipeline (`next/font`); everywhere else you self-host with [Fontsource](https://fontsource.org)
or a plain `@font-face`. All of these self-host the files, which is faster and more
private than linking to a third-party CDN.
### Next.js — `next/font` [#nextjs--nextfont]
`next/font` downloads the font at build time, self-hosts it, and hands you a CSS
variable with zero layout shift. Use `next/font/google` for a Google font:
```tsx title="app/layout.tsx"
import { Geist } from 'next/font/google'
const geist = Geist({
subsets: ['latin'],
variable: '--font-geist',
display: 'swap',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
)
}
```
…or `next/font/local` for font files you ship yourself:
```tsx title="app/layout.tsx"
import localFont from 'next/font/local'
const brand = localFont({
src: [
{ path: './fonts/Brand-Regular.woff2', weight: '400', style: 'normal' },
{ path: './fonts/Brand-Bold.woff2', weight: '700', style: 'normal' },
],
variable: '--font-brand',
display: 'swap',
})
```
Either way, map the generated variable onto the Appica token:
```css title="app/globals.css"
:root {
--font-sans: var(--font-geist), ui-sans-serif, system-ui, sans-serif;
}
```
`next/font` only exposes `--font-geist` on the element you put `geist.variable` on (and its
descendants), so apply it to ``. Don't also spread `geist.className` onto `` — that hardcodes
the font on the body and bypasses the token, so component-level `font-mono` and any later override stop
working. Pick the variable approach and let `--font-sans` flow through.
### Vite, TanStack Start, React Router 7 / Remix — Fontsource [#vite-tanstack-start-react-router-7--remix--fontsource]
These bundlers have no built-in font helper, so self-host with Fontsource — one npm
package per family, imported once:
```ts title="src/main.tsx"
import '@fontsource-variable/geist'
```
```css title="src/index.css"
:root {
--font-sans: 'Geist Variable', ui-sans-serif, system-ui, sans-serif;
}
```
The `font-family` Fontsource registers is shown on each font's page — variable fonts
use the `… Variable` suffix as above; static weights use the bare name (`'Geist'`).
### Any framework — local files with `@font-face` [#any-framework--local-files-with-font-face]
If you'd rather not add a dependency, declare the face yourself and drop the files in
your `public/` (or `static/`) folder. This works in every framework, including Astro:
```css title="globals.css"
@font-face {
font-family: 'Brand Sans';
src: url('/fonts/brand-sans.woff2') format('woff2');
font-weight: 100 900; /* a single variable file */
font-display: swap;
}
:root {
--font-sans: 'Brand Sans', ui-sans-serif, system-ui, sans-serif;
}
```
You *can* load a font with Google's `` snippet and it works
anywhere, but it adds a third-party request, an extra DNS round-trip, and sends your visitors' IPs to
Google. `next/font`, Fontsource, and `@font-face` all serve the files from your own origin — faster and
privacy-friendly. Reach for the link tag only as a quick prototype.
## The monospace font [#the-monospace-font]
`--font-mono` works identically — load the font, then redefine the token. It feeds
`font-mono`, code blocks, and anything that opts into monospace:
```css
:root {
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}
```
## Framework notes [#framework-notes]
The CSS override is identical everywhere; only the loading mechanism and where it
lives change:
| Framework | How to load | Where |
| ----------------------------------------------------------------------------------------------------------- | ---------------------------------- | --------------------------------- |
| Next.js (App Router) | `next/font` → CSS variable | `app/layout.tsx` + `globals.css` |
| Vite / CRA | Fontsource import or `@font-face` | `src/main.tsx` + `src/index.css` |
| TanStack Start | Fontsource import or `@font-face` | root route + `src/styles/app.css` |
| React Router 7 / Remix | Fontsource import or `@font-face` | `app/root.tsx` + `app/app.css` |
| Astro | `@font-face` (or Astro's font API) | `src/styles/global.css` |
Whatever the source, the contract is the same: make a `font-family` name resolvable
on the page, then put it first in `--font-sans` / `--font-mono` with the original
stack trailing as a fallback.
## Next steps [#next-steps]
* [Theming](/ui/docs/react/theming) — the token system fonts plug into.
* [Colors](/ui/docs/react/colors) — recolor with the same override pattern.
* [Dark Mode](/ui/docs/react/dark-mode) — switch and persist themes.
# Forms & Validation (/ui/docs/react/forms)
Forms are built from two pieces: `Field`, which groups a control with its label,
description, and error message and wires up the accessibility relationships; and
`Form`, which coordinates the fields and surfaces server-side errors. Both wrap
[Base UI](https://base-ui.com) primitives.
## Anatomy of a field [#anatomy-of-a-field]
A `Field` composes a label, a control, an optional description, and an error
slot. The `for`/`id` and `aria-describedby` associations are handled for you.
```tsx
import { Field, FieldLabel, FieldDescription, FieldError } from '@appica/ui-react/field'
import { Input } from '@appica/ui-react/input'
export default function EmailField() {
return (
EmailWe'll only use this to send receipts.
)
}
```
## Validation [#validation]
### Native constraints [#native-constraints]
Standard HTML attributes — `required`, `type`, `min`, `maxLength`, `pattern` —
are validated automatically. Point `FieldError` at a specific failure with
`match`, or render the browser's default message with a bare ``:
```tsx
EmailAn email is required.Enter a valid email address.
```
### Custom validation [#custom-validation]
For rules HTML can't express, pass a `validate` function to the `Field`. Return a
string (or array of strings) to fail, or `null` to pass. Control when it runs
with `validationMode` (`'onBlur'` or `'onChange'`):
```tsx
(String(value).length < 3 ? 'At least 3 characters.' : null)}
>
Username
```
`validate` can be async — return a promise — which is handy for availability
checks against an API.
### Shake on invalid [#shake-on-invalid]
For an extra cue when validation fails, hang the `animate-shake` utility off the
control's invalid state. Base UI sets `data-invalid` on the control when the field
fails validation, so put the class on the `Input` to shake just the control:
```tsx
Email
```
The shake plays once, when the field *becomes* invalid (the moment `data-invalid` is added) — not on every failed
submit while it stays invalid. Gating it behind `motion-safe:` keeps it off for users who prefer reduced motion, per
[Animation](/ui/docs/react/animation).
## Grouping with Fieldset [#grouping-with-fieldset]
Use `Fieldset` and `FieldsetLegend` to group related fields under a shared label
(an accessible `