# Base UI

You are working in a React 19 project that builds its components on Base UI (`@base-ui/react` 1.x) and TypeScript, usually styled with Tailwind CSS v4. Write code for the version in `package.json`, not the one in your training data: Base UI reached 1.0 after most of it, and its APIs differ from Radix in ways that compile but behave wrongly.

## Before you write code

- Read the docs the package ships: `node_modules/@base-ui/react/docs/react/components/<name>.md` (anatomy, examples, every prop, data attribute and CSS variable) and `handbook/` (composition, styling, animation, forms, customization). They are authoritative over what you remember.
- The package is `@base-ui/react`. The old name `@base-ui-components/react` must not appear in imports or installs.
- Check `components/ui/` (or the project's equivalent) first. If wrappers exist, as in shadcn/ui on Base UI, use and extend them instead of importing primitives into pages.
- Portaled popups need the app root to set `isolation: isolate` so they stack above page content without z-index fights; backdrops on iOS 26+ need `body { position: relative }`. Add both once if missing.

## Imports and anatomy

- Import each component from its subpath; parts are properties of it: `import { Popover } from "@base-ui/react/popover"`, then `Popover.Root`, `Popover.Trigger`, `Popover.Portal`, `Popover.Positioner`, `Popover.Popup`, `Popover.Arrow`, `Popover.Title`, `Popover.Description`, `Popover.Close`.
- Floating popups (Popover, Menu, Select, Combobox, Tooltip, Preview Card) nest Portal > Positioner > Popup. Positioning props (`side`, `align`, `sideOffset`, `alignOffset`, `collisionPadding`) go on the Positioner, never the Popup. Dialog and Alert Dialog have no Positioner: Portal > Backdrop + Popup (inside `Dialog.Viewport` when the dialog scrolls).
- Types live on the part's namespace: `Menu.Root.Props`, `Select.Trigger.State`, `Combobox.Root.ChangeEventDetails`. Type wrappers with them instead of hand-written prop interfaces.
- Files that use Base UI components are Client Components in Next.js: start them with `"use client"`.

## Composition with render

Base UI has no `asChild`. Use `render` to compose a part with your own component or to change its element:

```tsx
<Menu.Trigger render={<Button variant="outline" />}>Options</Menu.Trigger>
<Tooltip.Trigger render={<Dialog.Trigger render={<Button size="icon" aria-label="Share" />} />}>
  <ShareIcon />
</Tooltip.Trigger>
<Switch.Thumb render={(props, state) => <span {...props}>{state.checked ? <On /> : <Off />}</span>} />
```

- The element form merges Base UI's props into your element. The function form gets `(props, state)`: spread `props` on the root element every time, it carries the ref, handlers and ARIA attributes.
- A component passed to `render` must accept `ref` as a prop and spread the rest onto its DOM node (React 19: no `forwardRef`).
- `nativeButton`: triggers, Button and close parts default to `true`. When `render` produces something that is not a `<button>`, pass `nativeButton={false}`. Items, Checkbox, Radio and Switch default to `false`; pass `true` if you render a `<button>`.
- Do not render links through Button or a trigger. Style the `<a>` (or framework `Link`) directly; inside menus use `Menu.LinkItem`.
- Base UI's Button renders `type="button"`: write `type="submit"` explicitly on submit buttons.

## Styling

- Parts are unstyled. Style with `className` (a string, or a function of state: `className={(state) => …}`), `style` (an object or a function of state), data attributes and CSS variables.
- Base UI has no `data-state` attribute. Target the real ones: `data-open` and `data-closed`, `data-starting-style` and `data-ending-style` (enter and exit), `data-side` and `data-align` (Positioner and Popup), `data-popup-open` and `data-pressed` (triggers), `data-highlighted` (menu, select and combobox items), `data-selected` (select and combobox items), `data-checked`, `data-unchecked`, `data-indeterminate`, `data-disabled`, `data-panel-open` (accordion and collapsible triggers), `data-active` (tabs), and on Field parts `data-invalid`, `data-valid`, `data-dirty`, `data-touched`, `data-filled`, `data-focused`. Confirm names in the component's doc page.
- In Tailwind v4 they are plain variants: `data-highlighted:bg-accent`, `data-disabled:opacity-50`, `data-[side=top]:mb-1`.
- Use the CSS variables instead of measuring in JavaScript: on Positioner and Popup `--anchor-width`, `--anchor-height`, `--available-width`, `--available-height`, `--transform-origin`; `--accordion-panel-height` and `--collapsible-panel-height` on panels; `--active-tab-left` and `--active-tab-width` on `Tabs.Indicator`. For example `origin-(--transform-origin) max-h-(--available-height) min-w-(--anchor-width)`.

## Animation

- Prefer CSS transitions with `data-starting-style` and `data-ending-style`: they cancel smoothly when a popup closes mid-open. `transition-[opacity,scale] data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:scale-95 data-ending-style:opacity-0`.
- For keyframe animations use `data-open` and `data-closed`. Base UI keeps the popup mounted until the animation finishes, then calls `onOpenChangeComplete`.
- With Motion: control `open`, put `keepMounted` on the Portal, wrap it in `AnimatePresence`, and pass `render={<motion.div … />}` to the Popup. Animate `opacity` so Base UI detects the exit.

## State and events

- Uncontrolled by default (`defaultValue`, `defaultOpen`). To control, pass `value` with `onValueChange` or `open` with `onOpenChange`.
- Change handlers receive `(value, eventDetails)`. `eventDetails.reason` says why (`"escape-key"`, `"outside-press"`, `"item-press"`, `"trigger-press"`…), `eventDetails.cancel()` stops the change without controlling state, `eventDetails.allowPropagation()` lets Escape reach a parent.
- To open a dialog, popover or menu from elsewhere, create a handle (`Dialog.createHandle()`) and pass it as `handle` to the Root and to triggers placed anywhere; `handle.open()` works while that Root is mounted.
- Select: pass `items` to `Select.Root` so `Select.Value` shows labels instead of raw values; `placeholder` on `Select.Value`; `multiple` for multi-select; object values need `itemToStringValue` (and `isItemEqualToValue` for refetched objects). The popup overlaps the trigger by default (`alignItemWithTrigger`); set it `false` on the Positioner for a dropdown below.
- Combobox: pass `items` to the Root and render `<Combobox.List>{(item) => <Combobox.Item key={item.value} value={item}>…</Combobox.Item>}</Combobox.List>` with a `Combobox.Empty`. Filtering is built in; pass `filteredItems` for server results; use `Combobox.createItems` when selection should be an ID. Use Autocomplete for free text, Select when there is no input.
- Menu: `Menu.Item` takes `onClick` (it closes the menu unless `closeOnClick={false}`), not `onSelect`. `Menu.GroupLabel` goes inside `Menu.Group` or `Menu.RadioGroup`; submenus are `Menu.SubmenuRoot` with `Menu.SubmenuTrigger`.
- Dialog: always render `Dialog.Title` (and `Dialog.Description` when there is one). Use Alert Dialog for confirmations. `initialFocus` and `finalFocus` on the Popup move focus deliberately.
- Tooltip: wrap the app once in `Tooltip.Provider` so tooltips share delays. A tooltip supplements a name, never replaces it: icon-only triggers still need `aria-label`, and nothing interactive goes inside.
- Toast: render `Toast.Provider` once with a `Toast.Viewport` that maps `useToastManager().toasts` to `Toast.Root`; add toasts with `Toast.useToastManager().add({ title, description })`, or through `Toast.createToastManager()` outside React.

## Forms

- Compose `Field.Root` (with `name`) > `Field.Label`, `Field.Control` or another control, `Field.Description`, `Field.Error`. Group with `Fieldset.Root` and `Fieldset.Legend`; wrap each checkbox or radio option in `Field.Item`. Select, Slider and an input-in-popup Combobox use their own `Label` part.
- `Form` validates natively (`required`, `minLength`, `pattern`) and with `validate` on `Field.Root` (sync or async, returns a message or an array of them). Choose when with `validationMode` (`"onSubmit"` default, `"onBlur"`, `"onChange"`) and debounce with `validationDebounceTime`.
- `Field.Error` with no children shows the browser's message; `match="valueMissing"` (or another validity key) shows yours for that case.
- `onFormSubmit(values, eventDetails)` gives values as an object and prevents the native submit. For server validation, pass errors keyed by field name to `<Form errors={…}>`, for example from `useActionState`.
- With React Hook Form, wrap controls in `Controller` and pass `invalid`, `touched` and `dirty` from `fieldState` to `Field.Root`, plus `inputRef` so invalid fields get focus. With TanStack Form, drop the Base UI `Form`.

## Your own components

Give your own components the same `render` prop with `useRender` and `mergeProps`:

```tsx
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";

interface TagProps extends useRender.ComponentProps<"span"> {}

export function Tag({ render, ...props }: TagProps) {
  return useRender({
    defaultTagName: "span",
    render,
    props: mergeProps<"span">({ className: "inline-flex items-center gap-1" }, props),
  });
}
```

- Pass `state` to `useRender` to expose it to `render` functions and as `data-*` attributes; pass an internal ref as `ref` to merge it with the consumer's.
- `mergeProps` chains handlers (rightmost first), joins `className` and `style`, and keeps only the rightmost `ref`. In a merged handler, `event.preventBaseUIHandler()` skips Base UI's own logic.

## Do not

- Do not write `asChild`, `Slot`, Radix imports, `data-[state=open]:` selectors, `onSelect` on menu items or `type="single"` props: those are Radix APIs.
- Do not wrap components in `forwardRef`, import from internal paths, or use `@base-ui-components/react`.
- Do not put `side` or `sideOffset` on the Popup, measure the trigger with JavaScript, or add `z-50` to popups to fix stacking.
- Do not animate exits with conditional rendering of the Popup: Base UI unmounts it after the exit animation itself.
- Do not render an `<a>` as a Button, or a `<div>` trigger without `nativeButton={false}`.

## Before you finish

- Run the type checker and the linter and fix what they report.
- Use every component you touched from the keyboard: Tab to it, open with Enter, Space or the arrow keys, move with the arrows and typeahead, close with Escape, and check focus returns to the trigger.
- Check each popup's open and close animation in both directions, its position near viewport edges, and its contrast in light and dark mode.
