references/csp-provider.md
---
title: CSP Provider
subtitle: Configures CSP-related behavior for inline tags rendered by Base UI components.
description: A CSP provider component that applies a nonce to inline <style> and <script> tags rendered by Base UI components, and can disable inline <style> elements.
---
# CSP Provider
A CSP provider component that applies a nonce to inline \<style> and \<script> tags rendered by Base UI components, and can disable inline \<style> elements.
## Anatomy
Import the component and wrap it around your app:
```jsx title="Anatomy"
import { CSPProvider } from '@base-ui/react/csp-provider';
// prettier-ignore
<CSPProvider nonce="...">
{/* Your app or a group of components */}
</CSPProvider>
```
Some Base UI components render inline `<style>` or `<script>` tags for functionality such as removing scrollbars or pre-hydration behavior. Under a strict Content Security Policy (CSP), these tags may be blocked unless they include a matching [nonce](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/nonce) attribute.
`CSPProvider` allows configuring this behavior globally for all Base UI components within its tree.
## Supplying a nonce
If you enforce a CSP that blocks inline tags by default, configure your server to:
1. Generate a random nonce per request
2. Include it in your CSP header (via `style-src-elem`/`script-src`)
3. Pass the same nonce into `CSPProvider` during rendering
```ts title="Example"
const nonce = crypto.randomUUID();
// Example CSP header
const csp = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}'`,
`style-src-elem 'self' 'nonce-${nonce}'`,
].join('; ');
```
Then:
```jsx title="Providing the nonce"
import { CSPProvider } from '@base-ui/react/csp-provider';
function App({ nonce }) {
return <CSPProvider nonce={nonce}>{/* ... */}</CSPProvider>;
}
```
This will ensure that all inline `<style>` and `<script>` tags rendered by Base UI components include the correct nonce attribute, allowing them to function under your CSP.
## Disable inline style elements
You can avoid supplying a `nonce` if you disable inline `<style>` elements entirely and rely on external stylesheets only. The relevant components are `<ScrollArea.Viewport>` and `<Select.Popup>` or `<Select.List>` when `alignItemWithTrigger` is enabled, which inject a style tag to disable native scrollbars.
```html
<style>
.base-ui-disable-scrollbar {
scrollbar-width: none;
}
.base-ui-disable-scrollbar::-webkit-scrollbar {
display: none;
}
</style>
```
Specify `disableStyleElements` to remove these tags:
```jsx title="Disabling style elements"
<CSPProvider disableStyleElements>{/* ... */}</CSPProvider>
```
`<script>` tags across all components are opt-in, so they are not affected by this prop and don't have their own disable flag. A `nonce` is required if any component uses inline scripts.
## Inline style attributes
`CSPProvider` covers inline `<style>` and `<script>` tags rendered as elements, but it does not cover inline style attributes (for example, `<div style="...">`). The `style-src-attr` directive in CSP governs inline style attributes encountered when parsing HTML from server pre-rendered components (it does not affect client-side JavaScript that sets styles).
In CSP, `style-src` applies to both `<style>` elements and `style=""` attributes. If you only want to control `<style>` elements, use `style-src-elem` instead.
If your CSP blocks inline style _attributes_ in addition to _elements_, you have a few options:
1. Relax your CSP by adding `'unsafe-inline'` to the `style-src-attr` directive (or using only `style-src-elem` instead of `style-src`). Style attributes specifically pose a less severe security risk than style elements, but this approach may not be acceptable in high-security environments.
2. Render the affected components only on the client, so that no inline styles are present in the initial HTML.
3. Manually unset inline styles and specify them in your CSS instead. Any component can have its inline styles unset, such as `<ScrollArea.Viewport style={{ overflow: undefined }}>`. Note that you'll need to ensure you vet upgrades for any new inline styles added by Base UI components.
## API reference
Provides a default Content Security Policy (CSP) configuration for Base UI components that
require inline `<style>` or `<script>` tags.
**CSPProvider Props:**
| Prop | Type | Default | Description |
| :------------------- | :---------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| disableStyleElements | `boolean` | `false` | Whether inline `<style>` elements created by Base UI components should not be rendered. Instead, components must specify the CSS styles via custom class names or other methods. |
| nonce | `string` | - | The nonce value to apply to inline `<style>` and `<script>` tags. |
| children | `ReactNode` | - | - |
references/direction-provider.md
---
title: Direction Provider
subtitle: Enables RTL behavior for Base UI components.
description: A direction provider component that enables RTL behavior for Base UI components.
---
# Direction Provider
A direction provider component that enables RTL behavior for Base UI components.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import { Slider } from '@base-ui/react/slider';
import { DirectionProvider } from '@base-ui/react/direction-provider';
export default function ExampleDirectionProvider() {
return (
<div dir="rtl">
<DirectionProvider direction="rtl">
<Slider.Root defaultValue={25}>
<Slider.Control className="flex w-56 items-center py-3">
<Slider.Track className="relative h-1 w-full rounded bg-gray-200 shadow-[inset_0_0_0_1px] shadow-gray-200">
<Slider.Indicator className="rounded bg-gray-700" />
<Slider.Thumb className="size-4 rounded-full bg-white outline outline-1 outline-gray-300 has-[:focus-visible]:outline has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-blue-800" />
</Slider.Track>
</Slider.Control>
</Slider.Root>
</DirectionProvider>
</div>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Control {
box-sizing: border-box;
display: flex;
align-items: center;
width: 14rem;
padding-block: 0.75rem;
}
.Track {
width: 100%;
background-color: var(--color-gray-200);
box-shadow: inset 0 0 0 1px var(--color-gray-200);
height: 0.25rem;
border-radius: 0.25rem;
position: relative;
}
.Indicator {
border-radius: 0.25rem;
background-color: var(--color-gray-700);
}
.Thumb {
width: 1rem;
height: 1rem;
border-radius: 100%;
background-color: white;
outline: 1px solid var(--color-gray-300);
&:has(:focus-visible) {
outline: 2px solid var(--color-blue);
}
}
```
```tsx
/* index.tsx */
import { DirectionProvider } from '@base-ui/react/direction-provider';
import { Slider } from '@base-ui/react/slider';
import styles from './index.module.css';
export default function ExampleDirectionProvider() {
return (
<div dir="rtl">
<DirectionProvider direction="rtl">
<Slider.Root defaultValue={25}>
<Slider.Control className={styles.Control}>
<Slider.Track className={styles.Track}>
<Slider.Indicator className={styles.Indicator} />
<Slider.Thumb className={styles.Thumb} />
</Slider.Track>
</Slider.Control>
</Slider.Root>
</DirectionProvider>
</div>
);
}
```
## Anatomy
Import the component and wrap it around your app:
```jsx title="Anatomy"
import { DirectionProvider } from '@base-ui/react/direction-provider';
// prettier-ignore
<DirectionProvider>
{/* Your app or a group of components */}
</DirectionProvider>
```
`<DirectionProvider>` enables child Base UI components to adjust behavior based on RTL text direction, but does not affect HTML and CSS. The `dir="rtl"` HTML attribute or `direction: rtl` CSS style must be set additionally by your own application code.
## API reference
Enables RTL behavior for Base UI components.
**DirectionProvider Props:**
| Prop | Type | Default | Description |
| :-------- | :-------------- | :------ | :-------------------------------- |
| direction | `TextDirection` | `'ltr'` | The reading direction of the text |
| children | `ReactNode` | - | - |
## useDirection
Use this hook to read the current text direction. This is useful for wrapping portaled components that may be rendered outside your application root and are unaffected by the `dir` attribute set within.
### Return value
**Return Value:**
| Property | Type | Description |
| :-------- | :-------------- | :-------------------------- |
| direction | `TextDirection` | The current text direction. |
references/drawer.md
# Drawer
Base UI `Drawer` is a slide-in overlay primitive for mobile navigation, sheets, and bottom panels.
## v1.3.0 status
- `Drawer` is now stable and should be imported from `@base-ui/react/drawer`.
- Treat it as a first-class overlay primitive rather than a preview-only Sheet replacement.
- `Drawer.SwipeArea` is available when you want an explicit edge swipe affordance.
## Recommended anatomy
```tsx
import { Drawer } from "@base-ui/react/drawer";
<Drawer.Root>
<Drawer.Trigger />
<Drawer.Portal>
<Drawer.Backdrop />
<Drawer.Popup>
<Drawer.Handle />
<Drawer.Title />
<Drawer.Description />
<Drawer.Close />
</Drawer.Popup>
</Drawer.Portal>
</Drawer.Root>;
```
If you need touch-driven opening, add `Drawer.SwipeArea` near the screen edge instead of overloading the visible trigger.
## When to prefer Drawer
- Mobile bottom sheets
- Side panels that should feel modal
- Cases where touch gestures matter more than anchor positioning
Prefer `Popover` when the UI must stay anchored to a trigger and behave like a positioned popup.
## Practical notes
- Keep the interactive surface explicit: `Trigger` for click/tap, `SwipeArea` for gesture affordance.
- Avoid controlled swipe-dismiss flows unless you have a clear state-owner strategy; 1.3.0 specifically hardens controlled-drawer swipe behavior.
- Test nested scrolling and text selection on touch devices, especially for bottom sheets with forms.
## v1.4.0 notes
- Touch scrolling inside portaled popups is more reliable.
- Nested swipe-cancel and interrupted swipe-dismiss cleanup were fixed, so retest any local drawer gesture workarounds before keeping them.
- Base UI now warns when a drawer popup is missing `Viewport`; keep the full anatomy intact for swipe/stack correctness.
## v1.5.0 notes
- `Drawer.Viewport` now forwards `style`. Prefer that over wrapper div hacks when you need inline max-height, sizing, or animated viewport constraints.
## v1.9.0 notes
- Swipe release velocity is preserved on 120 Hz input and after a trailing stationary pointer event, so flick-to-dismiss no longer feels like it stops dead.
- `data-base-ui-swipe-ignore` accepts `x` and `y` options to ignore swipes along a single axis instead of both.
- A drag that starts on an SVG scrolls instead of swiping.
- Excess virtual-keyboard scroll space is removed, and item taps are preserved when the virtual keyboard is dismissed.
- CloseWatcher close requests coming from the `cancel` event are handled, and unmounted drawers are no longer retained in `<Drawer.Provider>`.
## Accessibility checklist
- Provide a visible title or an equivalent accessible label.
- Keep focus management inside the drawer when modal.
- Ensure dismiss controls exist even if swipe is enabled.
- Do not rely on swipe gestures as the only way to close the panel.
references/fieldset.md
---
title: Fieldset
subtitle: A native fieldset element with an easily stylable legend.
description: A high-quality, unstyled React fieldset component with an easily stylable legend.
---
# Fieldset
A high-quality, unstyled React fieldset component with an easily stylable legend.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import { Field } from "@base-ui/react/field";
import { Fieldset } from "@base-ui/react/fieldset";
export default function ExampleFieldset() {
return (
<Fieldset.Root className="flex w-full max-w-64 flex-col gap-4">
<Fieldset.Legend className="border-b border-gray-200 pb-3 text-lg font-medium text-gray-900">Billing details</Fieldset.Legend>
<Field.Root className="flex flex-col items-start gap-1">
<Field.Label className="text-sm font-medium text-gray-900">Company</Field.Label>
<Field.Control placeholder="Enter company name" className="h-10 w-full rounded-md border border-gray-200 pl-3.5 text-base text-gray-900 focus:outline focus:outline-2 focus:-outline-offset-1 focus:outline-blue-800" />
</Field.Root>
<Field.Root className="flex flex-col items-start gap-1">
<Field.Label className="text-sm font-medium text-gray-900">Tax ID</Field.Label>
<Field.Control placeholder="Enter fiscal number" className="h-10 w-full rounded-md border border-gray-200 pl-3.5 text-base text-gray-900 focus:outline focus:outline-2 focus:-outline-offset-1 focus:outline-blue-800" />
</Field.Root>
</Fieldset.Root>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Fieldset {
border: 0;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
max-width: 16rem;
}
.Legend {
border-bottom: 1px solid var(--color-gray-200);
padding-bottom: 0.75rem;
font-weight: 500;
font-size: 1.125rem;
line-height: 1.75rem;
letter-spacing: -0.0025em;
color: var(--color-gray-900);
}
.Field {
display: flex;
flex-direction: column;
align-items: start;
gap: 0.25rem;
}
.Label {
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
color: var(--color-gray-900);
}
.Input {
box-sizing: border-box;
padding-left: 0.875rem;
margin: 0;
border: 1px solid var(--color-gray-200);
width: 100%;
height: 2.5rem;
border-radius: 0.375rem;
font-family: inherit;
font-size: 1rem;
background-color: transparent;
color: var(--color-gray-900);
&:focus {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Error {
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--color-red-800);
}
.Description {
margin: 0;
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--color-gray-600);
}
```
```tsx
/* index.tsx */
import { Field } from "@base-ui/react/field";
import { Fieldset } from "@base-ui/react/fieldset";
import styles from "./index.module.css";
export default function ExampleFieldset() {
return (
<Fieldset.Root className={styles.Fieldset}>
<Fieldset.Legend className={styles.Legend}>Billing details</Fieldset.Legend>
<Field.Root className={styles.Field}>
<Field.Label className={styles.Label}>Company</Field.Label>
<Field.Control placeholder="Enter company name" className={styles.Input} />
</Field.Root>
<Field.Root className={styles.Field}>
<Field.Label className={styles.Label}>Tax ID</Field.Label>
<Field.Control placeholder="Enter fiscal number" className={styles.Input} />
</Field.Root>
</Fieldset.Root>
);
}
```
## Anatomy
Import the component and assemble its parts:
```jsx title="Anatomy"
import { Fieldset } from "@base-ui/react/fieldset";
<Fieldset.Root>
<Fieldset.Legend />
</Fieldset.Root>;
```
## API reference
### Root
Groups the fieldset legend and the associated fields.
Renders a `<fieldset>` element.
**Root Props:**
| Prop | Type | Default | Description |
| :-------- | :--------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Fieldset.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Fieldset.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Fieldset.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Legend
An accessible label that is automatically associated with the fieldset.
Renders a `<div>` element.
**Legend Props:**
| Prop | Type | Default | Description |
| :-------- | :----------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Fieldset.Legend.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Fieldset.Legend.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Fieldset.Legend.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
references/form.md
---
title: Form
subtitle: A native form element with consolidated error handling.
description: A high-quality, unstyled React form component with consolidated error handling.
---
# Form
A high-quality, unstyled React form component with consolidated error handling.
## v1.4.0 notes
- Hidden inputs used by Base UI controls now expose a `form` prop, which helps when the control lives outside the visual `<form>` element but still needs to submit into it.
- Hidden inputs also support `suppressHydrationWarning`, which is useful when SSR markup and client state can differ briefly during hydration.
## v1.5.0 notes
- Validation-sensitive form flows avoid older `flushSync` behavior in the `1.5.0` line. Remove timing hacks only after retesting real submit and error-render paths.
## v1.9.0 notes
- No warning is emitted when an uncontrolled `<Field.Control>`'s `defaultValue` changes.
- Label focus works in Shadow DOM.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Field } from "@base-ui/react/field";
import { Form } from "@base-ui/react/form";
import { Button } from "@base-ui/react/button";
export default function ExampleForm() {
const [errors, setErrors] = React.useState({});
const [loading, setLoading] = React.useState(false);
return (
<Form
className="flex w-full max-w-64 flex-col gap-4"
errors={errors}
onSubmit={async (event) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const value = formData.get("url") as string;
setLoading(true);
const response = await submitForm(value);
const serverErrors = {
url: response.error,
};
setErrors(serverErrors);
setLoading(false);
}}
>
<Field.Root name="url" className="flex flex-col items-start gap-1">
<Field.Label className="text-sm font-medium text-gray-900">Homepage</Field.Label>
<Field.Control type="url" required defaultValue="https://example.com" placeholder="https://example.com" pattern="https?://.*" className="h-10 w-full rounded-md border border-gray-200 pl-3.5 text-base text-gray-900 focus:outline focus:outline-2 focus:-outline-offset-1 focus:outline-blue-800" />
<Field.Error className="text-sm text-red-800" />
</Field.Root>
<Button disabled={loading} focusableWhenDisabled type="submit" className="flex items-center justify-center h-10 px-3.5 m-0 outline-0 border border-gray-200 rounded-md bg-gray-50 font-inherit text-base font-medium leading-6 text-gray-900 select-none hover:data-[disabled]:bg-gray-50 hover:bg-gray-100 active:data-[disabled]:bg-gray-50 active:bg-gray-200 active:shadow-[inset_0_1px_3px_rgba(0,0,0,0.1)] active:border-t-gray-300 active:data-[disabled]:shadow-none active:data-[disabled]:border-t-gray-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-800 focus-visible:-outline-offset-1 data-[disabled]:text-gray-500">
Submit
</Button>
</Form>
);
}
async function submitForm(value: string) {
// Mimic a server response
await new Promise((resolve) => {
setTimeout(resolve, 1000);
});
try {
const url = new URL(value);
if (url.hostname.endsWith("example.com")) {
return { error: "The example domain is not allowed" };
}
} catch {
return { error: "This is not a valid URL" };
}
return { success: true };
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Form {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
max-width: 16rem;
}
.Field {
display: flex;
flex-direction: column;
align-items: start;
gap: 0.25rem;
}
.Label {
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
color: var(--color-gray-900);
}
.Input {
box-sizing: border-box;
padding-left: 0.875rem;
margin: 0;
border: 1px solid var(--color-gray-200);
width: 100%;
height: 2.5rem;
border-radius: 0.375rem;
font-family: inherit;
font-size: 1rem;
background-color: transparent;
color: var(--color-gray-900);
&:focus {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Error {
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--color-red-800);
}
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover:not([data-disabled]) {
background-color: var(--color-gray-100);
}
}
&:active:not([data-disabled]) {
background-color: var(--color-gray-200);
box-shadow: inset 0 1px 3px var(--color-gray-200);
border-top-color: var(--color-gray-300);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
&[data-disabled] {
color: var(--color-gray-500);
}
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Field } from "@base-ui/react/field";
import { Form } from "@base-ui/react/form";
import { Button } from "@base-ui/react/button";
import styles from "./index.module.css";
export default function ExampleForm() {
const [errors, setErrors] = React.useState({});
const [loading, setLoading] = React.useState(false);
return (
<Form
className={styles.Form}
errors={errors}
onSubmit={async (event) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const value = formData.get("url") as string;
setLoading(true);
const response = await submitForm(value);
const serverErrors = {
url: response.error,
};
setErrors(serverErrors);
setLoading(false);
}}
>
<Field.Root name="url" className={styles.Field}>
<Field.Label className={styles.Label}>Homepage</Field.Label>
<Field.Control type="url" required defaultValue="https://example.com" placeholder="https://example.com" pattern="https?://.*" className={styles.Input} />
<Field.Error className={styles.Error} />
</Field.Root>
<Button type="submit" disabled={loading} focusableWhenDisabled className={styles.Button}>
Submit
</Button>
</Form>
);
}
async function submitForm(value: string) {
// Mimic a server response
await new Promise((resolve) => {
setTimeout(resolve, 1000);
});
try {
const url = new URL(value);
if (url.hostname.endsWith("example.com")) {
return { error: "The example domain is not allowed" };
}
} catch {
return { error: "This is not a valid URL" };
}
return { success: true };
}
```
## Anatomy
Form is composed together with [Field](/react/components/field.md). Import the components and place them together:
```jsx title="Anatomy"
import { Field } from "@base-ui/react/field";
import { Form } from "@base-ui/react/form";
<Form>
<Field.Root>
<Field.Label />
<Field.Control />
<Field.Error />
</Field.Root>
</Form>;
```
## Examples
### Submit with a Server Function
Forms using `useActionState` can be submitted with a [Server Function](https://react.dev/reference/react-dom/components/form#handle-form-submission-with-a-server-function) instead of `onSubmit`.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Field } from "@base-ui/react/field";
import { Form } from "@base-ui/react/form";
import { Button } from "@base-ui/react/button";
interface FormState {
serverErrors?: Form.Props["errors"];
success?: boolean;
}
export default function ActionStateForm() {
const [state, formAction, loading] = React.useActionState<FormState, FormData>(submitForm, {});
return (
<Form action={formAction} errors={state.serverErrors} className="flex w-full max-w-64 flex-col gap-4">
<Field.Root name="username" className="flex flex-col items-start gap-1">
<Field.Label className="text-sm font-medium text-gray-900">Username</Field.Label>
<Field.Control type="text" required defaultValue="admin" placeholder="e.g. alice132" className="h-10 w-full rounded-md border border-gray-200 pl-3.5 text-base text-gray-900 focus:outline focus:outline-2 focus:-outline-offset-1 focus:outline-blue-800" />
<Field.Error className="text-sm text-red-800" />
</Field.Root>
<Button type="submit" disabled={loading} focusableWhenDisabled className="flex items-center justify-center h-10 px-3.5 m-0 outline-0 border border-gray-200 rounded-md bg-gray-50 font-inherit text-base font-medium leading-6 text-gray-900 select-none hover:data-[disabled]:bg-gray-50 hover:bg-gray-100 active:data-[disabled]:bg-gray-50 active:bg-gray-200 active:shadow-[inset_0_1px_3px_rgba(0,0,0,0.1)] active:border-t-gray-300 active:data-[disabled]:shadow-none active:data-[disabled]:border-t-gray-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-800 focus-visible:-outline-offset-1 data-[disabled]:text-gray-500">
Submit
</Button>
</Form>
);
}
// Mark this as a Server Function with `'use server'` in a supporting framework like Next.js
async function submitForm(_previousState: FormState, formData: FormData) {
// Mimic a server response
await new Promise((resolve) => {
setTimeout(resolve, 1000);
});
try {
const username = formData.get("username") as string | null;
if (username === "admin") {
return { success: false, serverErrors: { username: "'admin' is reserved for system use" } };
}
// 50% chance the username is taken
const success = Math.random() > 0.5;
if (!success) {
return {
serverErrors: { username: `${username} is unavailable` },
};
}
} catch {
return { serverErrors: { username: "A server error has occurred" } };
}
return {};
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Form {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
max-width: 16rem;
}
.Field {
display: flex;
flex-direction: column;
align-items: start;
gap: 0.25rem;
}
.Label {
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
color: var(--color-gray-900);
}
.Input {
box-sizing: border-box;
padding-left: 0.875rem;
margin: 0;
border: 1px solid var(--color-gray-200);
width: 100%;
height: 2.5rem;
border-radius: 0.375rem;
font-family: inherit;
font-size: 1rem;
background-color: transparent;
color: var(--color-gray-900);
&:focus {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Error {
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--color-red-800);
}
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
&[data-disabled] {
color: var(--color-gray-500);
}
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Field } from "@base-ui/react/field";
import { Form } from "@base-ui/react/form";
import { Button } from "@base-ui/react/button";
import styles from "./index.module.css";
interface FormState {
serverErrors?: Form.Props["errors"];
success?: boolean;
}
export default function ActionStateForm() {
const [state, formAction, loading] = React.useActionState<FormState, FormData>(submitForm, {});
return (
<Form errors={state.serverErrors} action={formAction} className={styles.Form}>
<Field.Root name="username" className={styles.Field}>
<Field.Label className={styles.Label}>Username</Field.Label>
<Field.Control type="text" required defaultValue="admin" placeholder="e.g. alice132" className={styles.Input} />
<Field.Error className={styles.Error} />
</Field.Root>
<Button type="submit" disabled={loading} focusableWhenDisabled className={styles.Button}>
Submit
</Button>
</Form>
);
}
// Mark this as a Server Function with `'use server'` in a supporting framework like Next.js
async function submitForm(_previousState: FormState, formData: FormData) {
// Mimic a server response
await new Promise((resolve) => {
setTimeout(resolve, 1000);
});
try {
const username = formData.get("username") as string | null;
if (username === "admin") {
return { success: false, serverErrors: { username: "'admin' is reserved for system use" } };
}
// 50% chance the username is taken
const success = Math.random() > 0.5;
if (!success) {
return {
serverErrors: { username: `${username} is unavailable` },
};
}
} catch {
return { serverErrors: { username: "A server error has occurred" } };
}
return {};
}
```
### Submit form values as a JavaScript object
You can use `onFormSubmit` instead of the native `onSubmit` to access form values as a JavaScript object. This is useful when you need to transform the values before submission, or integrate with 3rd party APIs.
```tsx title="Submission using onFormSubmit"
<Form
onFormSubmit={async (formValues: { id: string; quantity: number }) => {
const payload = {
product_id: formValues.id,
order_quantity: formValues.quantity,
};
const response = await fetch("https://api.example.com", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
}}
/>
```
When used, `preventDefault` is called on the native submit event.
### Using with Zod
When parsing the schema using `schema.safeParse()`, the `z.flattenError(result.error).fieldErrors` data can be used to map the errors to each field's `name`.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { z } from "zod";
import { Field } from "@base-ui/react/field";
import { Form } from "@base-ui/react/form";
import { Button } from "@base-ui/react/button";
const schema = z.object({
name: z.string().min(1, "Name is required"),
age: z.coerce.number({ invalid_type_error: "Age must be a number" }).positive("Age must be a positive number"),
});
async function submitForm(formValues: Form.Values) {
const result = schema.safeParse(formValues);
if (!result.success) {
return {
errors: z.flattenError(result.error).fieldErrors,
};
}
return {
errors: {},
};
}
export default function Page() {
const [errors, setErrors] = React.useState({});
return (
<Form
className="flex w-full max-w-64 flex-col gap-4"
errors={errors}
onFormSubmit={async (formValues) => {
const response = await submitForm(formValues);
setErrors(response.errors);
}}
>
<Field.Root name="name" className="flex flex-col items-start gap-1">
<Field.Label className="text-sm font-medium text-gray-900">Name</Field.Label>
<Field.Control placeholder="Enter name" className="h-10 w-full rounded-md border border-gray-200 pl-3.5 text-base text-gray-900 focus:outline focus:outline-2 focus:-outline-offset-1 focus:outline-blue-800" />
<Field.Error className="text-sm text-red-800" />
</Field.Root>
<Field.Root name="age" className="flex flex-col items-start gap-1">
<Field.Label className="text-sm font-medium text-gray-900">Age</Field.Label>
<Field.Control placeholder="Enter age" className="h-10 w-full rounded-md border border-gray-200 pl-3.5 text-base text-gray-900 focus:outline focus:outline-2 focus:-outline-offset-1 focus:outline-blue-800" />
<Field.Error className="text-sm text-red-800" />
</Field.Root>
<Button type="submit" className="flex items-center justify-center h-10 px-3.5 m-0 outline-0 border border-gray-200 rounded-md bg-gray-50 font-inherit text-base font-medium leading-6 text-gray-900 select-none hover:data-[disabled]:bg-gray-50 hover:bg-gray-100 active:data-[disabled]:bg-gray-50 active:bg-gray-200 active:shadow-[inset_0_1px_3px_rgba(0,0,0,0.1)] active:border-t-gray-300 active:data-[disabled]:shadow-none active:data-[disabled]:border-t-gray-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-800 focus-visible:-outline-offset-1 data-[disabled]:text-gray-500">
Submit
</Button>
</Form>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Form {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
max-width: 16rem;
}
.Field {
display: flex;
flex-direction: column;
align-items: start;
gap: 0.25rem;
}
.Label {
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
color: var(--color-gray-900);
}
.Input {
box-sizing: border-box;
padding-left: 0.875rem;
margin: 0;
border: 1px solid var(--color-gray-200);
width: 100%;
height: 2.5rem;
border-radius: 0.375rem;
font-family: inherit;
font-size: 1rem;
background-color: transparent;
color: var(--color-gray-900);
&:focus {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Error {
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--color-red-800);
}
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover:not([data-disabled]) {
background-color: var(--color-gray-100);
}
}
&:active:not([data-disabled]) {
background-color: var(--color-gray-200);
box-shadow: inset 0 1px 3px var(--color-gray-200);
border-top-color: var(--color-gray-300);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
&[data-disabled] {
color: var(--color-gray-500);
}
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { z } from "zod";
import { Field } from "@base-ui/react/field";
import { Form } from "@base-ui/react/form";
import { Button } from "@base-ui/react/button";
import styles from "./index.module.css";
const schema = z.object({
name: z.string().min(1, "Name is required"),
age: z.coerce.number({ invalid_type_error: "Age must be a number" }).positive("Age must be a positive number"),
});
async function submitForm(formValues: Form.Values) {
const result = schema.safeParse(formValues);
if (!result.success) {
return {
errors: z.flattenError(result.error).fieldErrors,
};
}
return {
errors: {},
};
}
export default function Page() {
const [errors, setErrors] = React.useState({});
return (
<Form
className={styles.Form}
errors={errors}
onFormSubmit={async (formValues) => {
const response = await submitForm(formValues);
setErrors(response.errors);
}}
>
<Field.Root name="name" className={styles.Field}>
<Field.Label className={styles.Label}>Name</Field.Label>
<Field.Control placeholder="Enter name" className={styles.Input} />
<Field.Error className={styles.Error} />
</Field.Root>
<Field.Root name="age" className={styles.Field}>
<Field.Label className={styles.Label}>Age</Field.Label>
<Field.Control placeholder="Enter age" className={styles.Input} />
<Field.Error className={styles.Error} />
</Field.Root>
<Button type="submit" className={styles.Button}>
Submit
</Button>
</Form>
);
}
```
## API reference
A native form element with consolidated error handling.
Renders a `<form>` element.
**Form Props:**
| Prop | Type | Default | Description |
| :------------- | :----------------------------------------------------------------------------------- | :----------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| errors | `Errors` | - | Validation errors returned externally, typically after submission by a server or a form action.
This should be an object where keys correspond to the `name` attribute on `<Field.Root>`,
and values correspond to error(s) related to that field. |
| actionsRef | `RefObject<Form.Actions \| null>` | - | A ref to imperative actions.\* `validate`: Validates all fields when called. Optionally pass a field name to validate a single field. |
| onFormSubmit | `((formValues: Record<string, any>, eventDetails: Form.SubmitEventDetails) => void)` | - | Event handler called when the form is submitted.
`preventDefault()` is called on the native submit event when used. |
| validationMode | `FormValidationMode` | `'onSubmit'` | Determines when the form should be validated.
The `validationMode` prop on `<Field.Root>` takes precedence over this.
_ `onSubmit` (default): validates the field when the form is submitted, afterwards fields will re-validate on change.
_ `onBlur`: validates a field when it loses focus.
\* `onChange`: validates the field on every change to its value. |
| className | `string \| ((state: Form.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Form.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Form.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
references/input.md
---
title: Input
subtitle: A native input element that automatically works with Field.
description: A high-quality, unstyled React input component.
---
# Input
<Meta name="description" content="A high-quality, unstyled React input component." />
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import { Input } from "@base-ui/react/input";
export default function ExampleInput() {
return (
<label className="flex flex-col items-start gap-1">
<span className="text-sm font-medium text-gray-900">Name</span>
<Input placeholder="Enter your name" className="h-10 w-56 rounded-md border border-gray-200 pl-3.5 text-base text-gray-900 focus:outline focus:outline-2 focus:-outline-offset-1 focus:outline-blue-800" />
</label>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Label {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
color: var(--color-gray-900);
}
.Input {
box-sizing: border-box;
padding-left: 0.875rem;
margin: 0;
border: 1px solid var(--color-gray-200);
width: 14rem;
height: 2.5rem;
border-radius: 0.375rem;
font-family: inherit;
font-size: 1rem;
font-weight: normal;
background-color: transparent;
color: var(--color-gray-900);
&:focus {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
```
```tsx
/* index.tsx */
import { Input } from "@base-ui/react/input";
import styles from "./index.module.css";
export default function ExampleInput() {
return (
<label className={styles.Label}>
Name
<Input placeholder="Enter your name" className={styles.Input} />
</label>
);
}
```
## Usage guidelines
- **Form controls must have an accessible name**: It can be created using a `<label>` element or the `Field` component. See the [forms guide](/react/handbook/forms.md).
## Anatomy
Import the component and use it as a single part:
```jsx title="Anatomy"
import { Input } from "@base-ui/react/input";
<Input />;
```
## API reference
A native input element that automatically works with [Field](https://base-ui.com/react/components/field).
Renders an `<input>` element.
**Input Props:**
| Prop | Type | Default | Description |
| :------------ | :-------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| defaultValue | `string \| number \| string[]` | - | - |
| onValueChange | `((value: string, eventDetails: Field.Control.ChangeEventDetails) => void)` | - | Callback fired when the `value` changes. Use when controlled. |
| className | `string \| ((state: Input.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Input.State) => CSSProperties \| undefined)` | - | Inline styles for the input element, or a function that receives `Input.State` and returns styles (or `undefined`). |
| render | `ReactElement \| ((props: HTMLProps, state: Input.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Input Data Attributes:**
| Attribute | Type | Description |
| :------------ | :--- | :----------------------------------------------------------------------- |
| data-disabled | - | Present when the input is disabled. |
| data-valid | - | Present when the input is in valid state (when wrapped in Field.Root). |
| data-invalid | - | Present when the input is in invalid state (when wrapped in Field.Root). |
| data-dirty | - | Present when the input's value has changed (when wrapped in Field.Root). |
| data-touched | - | Present when the input has been touched (when wrapped in Field.Root). |
| data-filled | - | Present when the input is filled (when wrapped in Field.Root). |
| data-focused | - | Present when the input is focused (when wrapped in Field.Root). |
references/menu.md
---
title: Menu
subtitle: A list of actions in a dropdown, enhanced with keyboard navigation.
description: A high-quality, unstyled React menu component that displays list of actions in a dropdown, enhanced with keyboard navigation.
---
# Menu
A high-quality, unstyled React menu component that displays list of actions in a dropdown, enhanced with keyboard navigation.
## v1.5.0 notes
- Controlled `open` state now drives popup-open detection more reliably. If trigger styling depends on `data-[popup-open]`, re-test controlled menus before keeping old bridging logic.
## v1.9.0 notes
- Menu filtering is available as a preview. Wrap the root in `<Menu.FilterProvider>`, add `<Menu.Input>` to the popup, and put the items inside `<Menu.List>`. See the Filtering section below.
- `<Menu.Root>` accepts `onItemHighlighted`, which reports the highlighted item and why it changed.
- Automatic unmounting is no longer controlled through `actionsRef`. Call `eventDetails.preventUnmountOnClose()` in `onOpenChange` to opt out, then end the closing phase with `actionsRef.current.unmount()`. See `popover.md`.
- Nested menus without a root trigger are fixed, a submenu closes when focus returns through its guard, repeated trigger activation toggles submenus, and the focus guard no longer loops when the trigger is the only tabbable element.
- Typeahead state resets when the menu closes without moving focus, and the native event is passed to `onOpenChange` when an item press closes the menu.
- Hover-out animations are fixed when switching between detached triggers, and a viewport transition no longer starts when a retained popup reopens.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import * as React from "react";
import { Menu } from "@base-ui/react/menu";
export default function ExampleMenu() {
return (
<Menu.Root>
<Menu.Trigger className="flex h-10 items-center justify-center gap-1.5 rounded-md border border-gray-200 bg-gray-50 px-3.5 text-base font-medium text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100 data-[popup-open]:bg-gray-100">
Song <ChevronDownIcon className="-mr-1" />
</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner className="outline-none" sideOffset={8}>
<Menu.Popup className="origin-[var(--transform-origin)] rounded-md bg-[canvas] py-1 text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0 dark:shadow-none dark:-outline-offset-1 dark:outline-gray-300">
<Menu.Arrow className="data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180">
<ArrowSvg />
</Menu.Arrow>
<Menu.Item className="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">Add to Library</Menu.Item>
<Menu.Item className="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">Add to Playlist</Menu.Item>
<Menu.Separator className="mx-4 my-1.5 h-px bg-gray-200" />
<Menu.Item className="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">Play Next</Menu.Item>
<Menu.Item className="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">Play Last</Menu.Item>
<Menu.Separator className="mx-4 my-1.5 h-px bg-gray-200" />
<Menu.Item className="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">Favorite</Menu.Item>
<Menu.Item className="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">Share</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className="fill-[canvas]" />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className="fill-gray-200 dark:fill-none" />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className="dark:fill-gray-300" />
</svg>
);
}
function ChevronDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...props}>
<path d="M1 3.5L5 7.5L9 3.5" stroke="currentcolor" strokeWidth="1.5" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.ButtonIcon {
margin-right: -0.25rem;
}
.Positioner {
outline: 0;
}
.Popup {
box-sizing: border-box;
padding-block: 0.25rem;
border-radius: 0.375rem;
background-color: canvas;
color: var(--color-gray-900);
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.Item {
outline: 0;
cursor: default;
user-select: none;
padding-block: 0.5rem;
padding-left: 1rem;
padding-right: 2rem;
display: flex;
font-size: 0.875rem;
line-height: 1rem;
&[data-highlighted] {
z-index: 0;
position: relative;
color: var(--color-gray-50);
}
&[data-highlighted]::before {
content: "";
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0.25rem;
border-radius: 0.25rem;
background-color: var(--color-gray-900);
}
}
.Separator {
margin: 0.375rem 1rem;
height: 1px;
background-color: var(--color-gray-200);
}
```
```tsx
/* index.tsx */
import * as React from "react";
import { Menu } from "@base-ui/react/menu";
import styles from "./index.module.css";
export default function ExampleMenu() {
return (
<Menu.Root>
<Menu.Trigger className={styles.Button}>
Song <ChevronDownIcon className={styles.ButtonIcon} />
</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner className={styles.Positioner} sideOffset={8}>
<Menu.Popup className={styles.Popup}>
<Menu.Arrow className={styles.Arrow}>
<ArrowSvg />
</Menu.Arrow>
<Menu.Item className={styles.Item}>Add to Library</Menu.Item>
<Menu.Item className={styles.Item}>Add to Playlist</Menu.Item>
<Menu.Separator className={styles.Separator} />
<Menu.Item className={styles.Item}>Play Next</Menu.Item>
<Menu.Item className={styles.Item}>Play Last</Menu.Item>
<Menu.Separator className={styles.Separator} />
<Menu.Item className={styles.Item}>Favorite</Menu.Item>
<Menu.Item className={styles.Item}>Share</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className={styles.ArrowFill} />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className={styles.ArrowOuterStroke} />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className={styles.ArrowInnerStroke} />
</svg>
);
}
function ChevronDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...props}>
<path d="M1 3.5L5 7.5L9 3.5" stroke="currentcolor" strokeWidth="1.5" />
</svg>
);
}
```
## Anatomy
Import the component and assemble its parts:
```jsx title="Anatomy"
import { Menu } from "@base-ui/react/menu";
<Menu.FilterProvider>
<Menu.Root>
<Menu.Trigger />
<Menu.Portal>
<Menu.Backdrop />
<Menu.Positioner>
<Menu.Popup>
<Menu.Arrow />
<Menu.Input />
<Menu.Clear />
<Menu.Empty />
<Menu.List>
<Menu.Item />
<Menu.LinkItem />
<Menu.Separator />
<Menu.SubmenuRoot>
<Menu.SubmenuTrigger />
</Menu.SubmenuRoot>
<Menu.Group>
<Menu.GroupLabel />
</Menu.Group>
<Menu.RadioGroup>
<Menu.GroupLabel />
<Menu.RadioItem>
<Menu.RadioItemIndicator />
</Menu.RadioItem>
</Menu.RadioGroup>
<Menu.CheckboxItem>
<Menu.CheckboxItemIndicator />
</Menu.CheckboxItem>
</Menu.List>
<Menu.Viewport />
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
</Menu.FilterProvider>;
```
`Menu.FilterProvider`, `Menu.Input`, `Menu.Clear`, `Menu.Empty`, and `Menu.List` are only needed for filtering. A menu without filtering keeps the plain `Root` + `Trigger` + `Portal` + `Positioner` + `Popup` + `Item` structure.
## Examples
### Open on hover
To create a menu that opens on hover, add the `openOnHover` prop to `<Menu.Trigger>`. You can additionally configure how quickly the menu opens on hover using the `delay` prop.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import * as React from "react";
import { Menu } from "@base-ui/react/menu";
export default function ExampleMenu() {
return (
<Menu.Root>
<Menu.Trigger openOnHover className="flex h-10 items-center justify-center gap-1.5 rounded-md border border-gray-200 bg-gray-50 px-3.5 text-base font-medium text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100 data-[popup-open]:bg-gray-100">
Add to playlist <ChevronDownIcon className="-mr-1" />
</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner className="outline-none" sideOffset={8}>
<Menu.Popup className="origin-[var(--transform-origin)] rounded-md bg-[canvas] py-1 text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0 dark:shadow-none dark:-outline-offset-1 dark:outline-gray-300">
<Menu.Arrow className="data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180">
<ArrowSvg />
</Menu.Arrow>
<Menu.Item className="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">Get Up!</Menu.Item>
<Menu.Item className="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">Inside Out</Menu.Item>
<Menu.Item className="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">Night Beats</Menu.Item>
<Menu.Separator className="mx-4 my-1.5 h-px bg-gray-200" />
<Menu.Item className="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">New playlist…</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className="fill-[canvas]" />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className="fill-gray-200 dark:fill-none" />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className="dark:fill-gray-300" />
</svg>
);
}
function ChevronDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...props}>
<path d="M1 3.5L5 7.5L9 3.5" stroke="currentcolor" strokeWidth="1.5" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.ButtonIcon {
margin-right: -0.25rem;
}
.Positioner {
outline: 0;
}
.Popup {
box-sizing: border-box;
padding-block: 0.25rem;
border-radius: 0.375rem;
background-color: canvas;
color: var(--color-gray-900);
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.Item {
outline: 0;
cursor: default;
user-select: none;
padding-block: 0.5rem;
padding-left: 1rem;
padding-right: 2rem;
display: flex;
font-size: 0.875rem;
line-height: 1rem;
&[data-highlighted] {
z-index: 0;
position: relative;
color: var(--color-gray-50);
}
&[data-highlighted]::before {
content: "";
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0.25rem;
border-radius: 0.25rem;
background-color: var(--color-gray-900);
}
}
.Separator {
margin: 0.375rem 1rem;
height: 1px;
background-color: var(--color-gray-200);
}
```
```tsx
/* index.tsx */
import * as React from "react";
import { Menu } from "@base-ui/react/menu";
import styles from "./index.module.css";
export default function ExampleMenu() {
return (
<Menu.Root>
<Menu.Trigger openOnHover className={styles.Button}>
Add to playlist <ChevronDownIcon className={styles.ButtonIcon} />
</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner className={styles.Positioner} sideOffset={8}>
<Menu.Popup className={styles.Popup}>
<Menu.Arrow className={styles.Arrow}>
<ArrowSvg />
</Menu.Arrow>
<Menu.Item className={styles.Item}>Get Up!</Menu.Item>
<Menu.Item className={styles.Item}>Inside Out</Menu.Item>
<Menu.Item className={styles.Item}>Night Beats</Menu.Item>
<Menu.Separator className={styles.Separator} />
<Menu.Item className={styles.Item}>New playlist…</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className={styles.ArrowFill} />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className={styles.ArrowOuterStroke} />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className={styles.ArrowInnerStroke} />
</svg>
);
}
function ChevronDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...props}>
<path d="M1 3.5L5 7.5L9 3.5" stroke="currentcolor" strokeWidth="1.5" />
</svg>
);
}
```
### Checkbox items
Use the `<Menu.CheckboxItem>` part to create a menu item that can toggle a setting on or off.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Menu } from "@base-ui/react/menu";
export default function ExampleMenu() {
const [showMinimap, setShowMinimap] = React.useState(true);
const [showSearch, setShowSearch] = React.useState(true);
const [showSidebar, setShowSidebar] = React.useState(false);
return (
<Menu.Root>
<Menu.Trigger className="flex h-10 items-center justify-center gap-1.5 rounded-md border border-gray-200 bg-gray-50 px-3.5 text-base font-medium text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100 data-[popup-open]:bg-gray-100">
Workspace <ChevronDownIcon className="-mr-1" />
</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner className="outline-none" sideOffset={8}>
<Menu.Popup className="origin-[var(--transform-origin)] rounded-md bg-[canvas] py-1 text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0 dark:shadow-none dark:-outline-offset-1 dark:outline-gray-300">
<Menu.Arrow className="data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180">
<ArrowSvg />
</Menu.Arrow>
<Menu.CheckboxItem checked={showMinimap} onCheckedChange={setShowMinimap} className="grid cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
<Menu.CheckboxItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Menu.CheckboxItemIndicator>
<span className="col-start-2">Minimap</span>
</Menu.CheckboxItem>
<Menu.CheckboxItem checked={showSearch} onCheckedChange={setShowSearch} className="grid cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
<Menu.CheckboxItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Menu.CheckboxItemIndicator>
<span className="col-start-2">Search</span>
</Menu.CheckboxItem>
<Menu.CheckboxItem checked={showSidebar} onCheckedChange={setShowSidebar} className="grid cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
<Menu.CheckboxItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Menu.CheckboxItemIndicator>
<span className="col-start-2">Sidebar</span>
</Menu.CheckboxItem>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className="fill-[canvas]" />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className="fill-gray-200 dark:fill-none" />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className="dark:fill-gray-300" />
</svg>
);
}
function ChevronDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...props}>
<path d="M1 3.5L5 7.5L9 3.5" stroke="currentcolor" strokeWidth="1.5" />
</svg>
);
}
function CheckIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="10" height="10" viewBox="0 0 10 10" {...props}>
<path d="M9.1603 1.12218C9.50684 1.34873 9.60427 1.81354 9.37792 2.16038L5.13603 8.66012C5.01614 8.8438 4.82192 8.96576 4.60451 8.99384C4.3871 9.02194 4.1683 8.95335 4.00574 8.80615L1.24664 6.30769C0.939709 6.02975 0.916013 5.55541 1.19372 5.24822C1.47142 4.94102 1.94536 4.91731 2.2523 5.19524L4.36085 7.10461L8.12299 1.33999C8.34934 0.993152 8.81376 0.895638 9.1603 1.12218Z" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.ButtonIcon {
margin-right: -0.25rem;
}
.Positioner {
outline: 0;
}
.Popup {
box-sizing: border-box;
padding-block: 0.25rem;
border-radius: 0.375rem;
background-color: canvas;
color: var(--color-gray-900);
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.CheckboxItem {
outline: 0;
cursor: default;
user-select: none;
padding-block: 0.5rem;
padding-left: 0.625rem;
padding-right: 2rem;
font-size: 0.875rem;
line-height: 1rem;
display: grid;
gap: 0.5rem;
align-items: center;
grid-template-columns: 0.75rem 1fr;
&[data-highlighted] {
z-index: 0;
position: relative;
color: var(--color-gray-50);
}
&[data-highlighted]::before {
content: "";
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0.25rem;
border-radius: 0.25rem;
background-color: var(--color-gray-900);
}
}
.CheckboxItemIndicator {
grid-column-start: 1;
}
.CheckboxItemIndicatorIcon {
display: block;
width: 0.75rem;
height: 0.75rem;
}
.CheckboxItemText {
grid-column-start: 2;
}
.Separator {
margin: 0.375rem 1rem;
height: 1px;
background-color: var(--color-gray-200);
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Menu } from "@base-ui/react/menu";
import styles from "./index.module.css";
export default function ExampleMenu() {
const [showMinimap, setShowMinimap] = React.useState(true);
const [showSearch, setShowSearch] = React.useState(true);
const [showSidebar, setShowSidebar] = React.useState(false);
return (
<Menu.Root>
<Menu.Trigger className={styles.Button}>
Workspace <ChevronDownIcon className={styles.ButtonIcon} />
</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner className={styles.Positioner} sideOffset={8}>
<Menu.Popup className={styles.Popup}>
<Menu.Arrow className={styles.Arrow}>
<ArrowSvg />
</Menu.Arrow>
<Menu.CheckboxItem checked={showMinimap} onCheckedChange={setShowMinimap} className={styles.CheckboxItem}>
<Menu.CheckboxItemIndicator className={styles.CheckboxItemIndicator}>
<CheckIcon className={styles.CheckboxItemIndicatorIcon} />
</Menu.CheckboxItemIndicator>
<span className={styles.CheckboxItemText}>Minimap</span>
</Menu.CheckboxItem>
<Menu.CheckboxItem checked={showSearch} onCheckedChange={setShowSearch} className={styles.CheckboxItem}>
<Menu.CheckboxItemIndicator className={styles.CheckboxItemIndicator}>
<CheckIcon className={styles.CheckboxItemIndicatorIcon} />
</Menu.CheckboxItemIndicator>
<span className={styles.CheckboxItemText}>Search</span>
</Menu.CheckboxItem>
<Menu.CheckboxItem checked={showSidebar} onCheckedChange={setShowSidebar} className={styles.CheckboxItem}>
<Menu.CheckboxItemIndicator className={styles.CheckboxItemIndicator}>
<CheckIcon className={styles.CheckboxItemIndicatorIcon} />
</Menu.CheckboxItemIndicator>
<span className={styles.CheckboxItemText}>Sidebar</span>
</Menu.CheckboxItem>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className={styles.ArrowFill} />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className={styles.ArrowOuterStroke} />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className={styles.ArrowInnerStroke} />
</svg>
);
}
function ChevronDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...props}>
<path d="M1 3.5L5 7.5L9 3.5" stroke="currentcolor" strokeWidth="1.5" />
</svg>
);
}
function CheckIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="10" height="10" viewBox="0 0 10 10" {...props}>
<path d="M9.1603 1.12218C9.50684 1.34873 9.60427 1.81354 9.37792 2.16038L5.13603 8.66012C5.01614 8.8438 4.82192 8.96576 4.60451 8.99384C4.3871 9.02194 4.1683 8.95335 4.00574 8.80615L1.24664 6.30769C0.939709 6.02975 0.916013 5.55541 1.19372 5.24822C1.47142 4.94102 1.94536 4.91731 2.2523 5.19524L4.36085 7.10461L8.12299 1.33999C8.34934 0.993152 8.81376 0.895638 9.1603 1.12218Z" />
</svg>
);
}
```
### Radio items
Use the `<Menu.RadioGroup>` and `<Menu.RadioItem>` parts to create menu items that work like radio buttons.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Menu } from "@base-ui/react/menu";
export default function ExampleMenu() {
const [value, setValue] = React.useState("date");
return (
<Menu.Root>
<Menu.Trigger className="flex h-10 items-center justify-center gap-1.5 rounded-md border border-gray-200 bg-gray-50 px-3.5 text-base font-medium text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100 data-[popup-open]:bg-gray-100">
Sort <ChevronDownIcon className="-mr-1" />
</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner className="outline-none" sideOffset={8}>
<Menu.Popup className="origin-[var(--transform-origin)] rounded-md bg-[canvas] py-1 text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0 dark:shadow-none dark:-outline-offset-1 dark:outline-gray-300">
<Menu.Arrow className="data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180">
<ArrowSvg />
</Menu.Arrow>
<Menu.RadioGroup value={value} onValueChange={setValue}>
<Menu.RadioItem value="date" className="grid cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
<Menu.RadioItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Menu.RadioItemIndicator>
<span className="col-start-2">Date</span>
</Menu.RadioItem>
<Menu.RadioItem value="name" className="grid cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
<Menu.RadioItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Menu.RadioItemIndicator>
<span className="col-start-2">Name</span>
</Menu.RadioItem>
<Menu.RadioItem value="type" className="grid cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
<Menu.RadioItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Menu.RadioItemIndicator>
<span className="col-start-2">Type</span>
</Menu.RadioItem>
</Menu.RadioGroup>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className="fill-[canvas]" />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className="fill-gray-200 dark:fill-none" />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className="dark:fill-gray-300" />
</svg>
);
}
function ChevronDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...props}>
<path d="M1 3.5L5 7.5L9 3.5" stroke="currentcolor" strokeWidth="1.5" />
</svg>
);
}
function CheckIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="10" height="10" viewBox="0 0 10 10" {...props}>
<path d="M9.1603 1.12218C9.50684 1.34873 9.60427 1.81354 9.37792 2.16038L5.13603 8.66012C5.01614 8.8438 4.82192 8.96576 4.60451 8.99384C4.3871 9.02194 4.1683 8.95335 4.00574 8.80615L1.24664 6.30769C0.939709 6.02975 0.916013 5.55541 1.19372 5.24822C1.47142 4.94102 1.94536 4.91731 2.2523 5.19524L4.36085 7.10461L8.12299 1.33999C8.34934 0.993152 8.81376 0.895638 9.1603 1.12218Z" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.ButtonIcon {
margin-right: -0.25rem;
}
.Positioner {
outline: 0;
}
.Popup {
box-sizing: border-box;
padding-block: 0.25rem;
border-radius: 0.375rem;
background-color: canvas;
color: var(--color-gray-900);
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.RadioItem {
outline: 0;
cursor: default;
user-select: none;
padding-block: 0.5rem;
padding-left: 0.625rem;
padding-right: 2rem;
font-size: 0.875rem;
line-height: 1rem;
display: grid;
gap: 0.5rem;
align-items: center;
grid-template-columns: 0.75rem 1fr;
&[data-highlighted] {
z-index: 0;
position: relative;
color: var(--color-gray-50);
}
&[data-highlighted]::before {
content: "";
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0.25rem;
border-radius: 0.25rem;
background-color: var(--color-gray-900);
}
}
.RadioItemIndicator {
grid-column-start: 1;
}
.RadioItemIndicatorIcon {
display: block;
width: 0.75rem;
height: 0.75rem;
}
.RadioItemText {
grid-column-start: 2;
}
.Separator {
margin: 0.375rem 1rem;
height: 1px;
background-color: var(--color-gray-200);
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Menu } from "@base-ui/react/menu";
import styles from "./index.module.css";
export default function ExampleMenu() {
const [value, setValue] = React.useState("date");
return (
<Menu.Root>
<Menu.Trigger className={styles.Button}>
Sort <ChevronDownIcon className={styles.ButtonIcon} />
</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner className={styles.Positioner} sideOffset={8}>
<Menu.Popup className={styles.Popup}>
<Menu.Arrow className={styles.Arrow}>
<ArrowSvg />
</Menu.Arrow>
<Menu.RadioGroup value={value} onValueChange={setValue}>
<Menu.RadioItem className={styles.RadioItem} value="date">
<Menu.RadioItemIndicator className={styles.RadioItemIndicator}>
<CheckIcon className={styles.RadioItemIndicatorIcon} />
</Menu.RadioItemIndicator>
<span className={styles.RadioItemText}>Date</span>
</Menu.RadioItem>
<Menu.RadioItem className={styles.RadioItem} value="name">
<Menu.RadioItemIndicator className={styles.RadioItemIndicator}>
<CheckIcon className={styles.RadioItemIndicatorIcon} />
</Menu.RadioItemIndicator>
<span className={styles.RadioItemText}>Name</span>
</Menu.RadioItem>
<Menu.RadioItem className={styles.RadioItem} value="type">
<Menu.RadioItemIndicator className={styles.RadioItemIndicator}>
<CheckIcon className={styles.RadioItemIndicatorIcon} />
</Menu.RadioItemIndicator>
<span className={styles.RadioItemText}>Type</span>
</Menu.RadioItem>
</Menu.RadioGroup>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className={styles.ArrowFill} />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className={styles.ArrowOuterStroke} />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className={styles.ArrowInnerStroke} />
</svg>
);
}
function ChevronDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...props}>
<path d="M1 3.5L5 7.5L9 3.5" stroke="currentcolor" strokeWidth="1.5" />
</svg>
);
}
function CheckIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="10" height="10" viewBox="0 0 10 10" {...props}>
<path d="M9.1603 1.12218C9.50684 1.34873 9.60427 1.81354 9.37792 2.16038L5.13603 8.66012C5.01614 8.8438 4.82192 8.96576 4.60451 8.99384C4.3871 9.02194 4.1683 8.95335 4.00574 8.80615L1.24664 6.30769C0.939709 6.02975 0.916013 5.55541 1.19372 5.24822C1.47142 4.94102 1.94536 4.91731 2.2523 5.19524L4.36085 7.10461L8.12299 1.33999C8.34934 0.993152 8.81376 0.895638 9.1603 1.12218Z" />
</svg>
);
}
```
### Close on click
Use the `closeOnClick` prop to change whether the menu closes when an item is clicked.
```jsx title="Control whether the menu closes on click"
// Close the menu when a checkbox item is clicked
<Menu.CheckboxItem closeOnClick />
// Keep the menu open when an item is clicked
<Menu.Item closeOnClick={false} />
```
### Group labels
Use the `<Menu.GroupLabel>` part to add a label to a `<Menu.Group>`
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Menu } from "@base-ui/react/menu";
export default function ExampleMenu() {
const [value, setValue] = React.useState("date");
const [showMinimap, setShowMinimap] = React.useState(true);
const [showSearch, setShowSearch] = React.useState(true);
const [showSidebar, setShowSidebar] = React.useState(false);
return (
<Menu.Root>
<Menu.Trigger className="flex h-10 items-center justify-center gap-1.5 rounded-md border border-gray-200 bg-gray-50 px-3.5 text-base font-medium text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100 data-[popup-open]:bg-gray-100">
View <ChevronDownIcon className="-mr-1" />
</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner className="outline-none" sideOffset={8}>
<Menu.Popup className="origin-[var(--transform-origin)] rounded-md bg-[canvas] py-1 text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0 dark:shadow-none dark:-outline-offset-1 dark:outline-gray-300">
<Menu.Arrow className="data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180">
<ArrowSvg />
</Menu.Arrow>
<Menu.Group>
<Menu.GroupLabel className="cursor-default py-2 pr-8 pl-7.5 text-sm leading-4 text-gray-600 select-none">Sort</Menu.GroupLabel>
<Menu.RadioGroup value={value} onValueChange={setValue}>
<Menu.RadioItem value="date" className="grid cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
<Menu.RadioItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Menu.RadioItemIndicator>
<span className="col-start-2">Date</span>
</Menu.RadioItem>
<Menu.RadioItem value="name" className="grid cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
<Menu.RadioItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Menu.RadioItemIndicator>
<span className="col-start-2">Name</span>
</Menu.RadioItem>
<Menu.RadioItem value="type" className="grid cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
<Menu.RadioItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Menu.RadioItemIndicator>
<span className="col-start-2">Type</span>
</Menu.RadioItem>
</Menu.RadioGroup>
</Menu.Group>
<Menu.Separator className="my-1.5 mr-4 ml-7.5 h-px bg-gray-200" />
<Menu.Group>
<Menu.GroupLabel className="cursor-default py-2 pr-8 pl-7.5 text-sm leading-4 text-gray-600 select-none">Workspace</Menu.GroupLabel>
<Menu.CheckboxItem checked={showMinimap} onCheckedChange={setShowMinimap} className="grid cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
<Menu.CheckboxItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Menu.CheckboxItemIndicator>
<span className="col-start-2">Minimap</span>
</Menu.CheckboxItem>
<Menu.CheckboxItem checked={showSearch} onCheckedChange={setShowSearch} className="grid cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
<Menu.CheckboxItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Menu.CheckboxItemIndicator>
<span className="col-start-2">Search</span>
</Menu.CheckboxItem>
<Menu.CheckboxItem checked={showSidebar} onCheckedChange={setShowSidebar} className="grid cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
<Menu.CheckboxItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Menu.CheckboxItemIndicator>
<span className="col-start-2">Sidebar</span>
</Menu.CheckboxItem>
</Menu.Group>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className="fill-[canvas]" />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className="fill-gray-200 dark:fill-none" />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className="dark:fill-gray-300" />
</svg>
);
}
function ChevronDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...props}>
<path d="M1 3.5L5 7.5L9 3.5" stroke="currentcolor" strokeWidth="1.5" />
</svg>
);
}
function CheckIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="10" height="10" viewBox="0 0 10 10" {...props}>
<path d="M9.1603 1.12218C9.50684 1.34873 9.60427 1.81354 9.37792 2.16038L5.13603 8.66012C5.01614 8.8438 4.82192 8.96576 4.60451 8.99384C4.3871 9.02194 4.1683 8.95335 4.00574 8.80615L1.24664 6.30769C0.939709 6.02975 0.916013 5.55541 1.19372 5.24822C1.47142 4.94102 1.94536 4.91731 2.2523 5.19524L4.36085 7.10461L8.12299 1.33999C8.34934 0.993152 8.81376 0.895638 9.1603 1.12218Z" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.ButtonIcon {
margin-right: -0.25rem;
}
.Positioner {
outline: 0;
}
.Popup {
box-sizing: border-box;
padding-block: 0.25rem;
border-radius: 0.375rem;
background-color: canvas;
color: var(--color-gray-900);
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.CheckboxItem,
.RadioItem {
outline: 0;
cursor: default;
user-select: none;
padding-block: 0.5rem;
padding-left: 0.625rem;
padding-right: 2rem;
font-size: 0.875rem;
line-height: 1rem;
display: grid;
gap: 0.5rem;
align-items: center;
grid-template-columns: 0.75rem 1fr;
&[data-highlighted] {
z-index: 0;
position: relative;
color: var(--color-gray-50);
}
&[data-highlighted]::before {
content: "";
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0.25rem;
border-radius: 0.25rem;
background-color: var(--color-gray-900);
}
}
.CheckboxItemIndicator,
.RadioItemIndicator {
grid-column-start: 1;
}
.CheckboxItemIndicatorIcon,
.RadioItemIndicatorIcon {
display: block;
width: 0.75rem;
height: 0.75rem;
}
.CheckboxItemText,
.RadioItemText {
grid-column-start: 2;
}
.Separator {
margin-block: 0.375rem;
margin-left: 1.875rem;
margin-right: 1rem;
height: 1px;
background-color: var(--color-gray-200);
}
.GroupLabel {
cursor: default;
user-select: none;
padding-block: 0.5rem;
padding-left: 1.875rem;
padding-right: 2rem;
font-size: 0.875rem;
line-height: 1rem;
color: var(--color-gray-600);
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Menu } from "@base-ui/react/menu";
import styles from "./index.module.css";
export default function ExampleMenu() {
const [value, setValue] = React.useState("date");
const [showMinimap, setShowMinimap] = React.useState(true);
const [showSearch, setShowSearch] = React.useState(true);
const [showSidebar, setShowSidebar] = React.useState(false);
return (
<Menu.Root>
<Menu.Trigger className={styles.Button}>
View <ChevronDownIcon className={styles.ButtonIcon} />
</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner className={styles.Positioner} sideOffset={8}>
<Menu.Popup className={styles.Popup}>
<Menu.Arrow className={styles.Arrow}>
<ArrowSvg />
</Menu.Arrow>
<Menu.Group>
<Menu.GroupLabel className={styles.GroupLabel}>Sort</Menu.GroupLabel>
<Menu.RadioGroup value={value} onValueChange={setValue}>
<Menu.RadioItem className={styles.RadioItem} value="date">
<Menu.RadioItemIndicator className={styles.RadioItemIndicator}>
<CheckIcon className={styles.RadioItemIndicatorIcon} />
</Menu.RadioItemIndicator>
<span className={styles.RadioItemText}>Date</span>
</Menu.RadioItem>
<Menu.RadioItem className={styles.RadioItem} value="name">
<Menu.RadioItemIndicator className={styles.RadioItemIndicator}>
<CheckIcon className={styles.RadioItemIndicatorIcon} />
</Menu.RadioItemIndicator>
<span className={styles.RadioItemText}>Name</span>
</Menu.RadioItem>
<Menu.RadioItem className={styles.RadioItem} value="type">
<Menu.RadioItemIndicator className={styles.RadioItemIndicator}>
<CheckIcon className={styles.RadioItemIndicatorIcon} />
</Menu.RadioItemIndicator>
<span className={styles.RadioItemText}>Type</span>
</Menu.RadioItem>
</Menu.RadioGroup>
</Menu.Group>
<Menu.Separator className={styles.Separator} />
<Menu.Group>
<Menu.GroupLabel className={styles.GroupLabel}>Workspace</Menu.GroupLabel>
<Menu.CheckboxItem checked={showMinimap} onCheckedChange={setShowMinimap} className={styles.CheckboxItem}>
<Menu.CheckboxItemIndicator className={styles.CheckboxItemIndicator}>
<CheckIcon className={styles.CheckboxItemIndicatorIcon} />
</Menu.CheckboxItemIndicator>
<span className={styles.CheckboxItemText}>Minimap</span>
</Menu.CheckboxItem>
<Menu.CheckboxItem checked={showSearch} onCheckedChange={setShowSearch} className={styles.CheckboxItem}>
<Menu.CheckboxItemIndicator className={styles.CheckboxItemIndicator}>
<CheckIcon className={styles.CheckboxItemIndicatorIcon} />
</Menu.CheckboxItemIndicator>
<span className={styles.CheckboxItemText}>Search</span>
</Menu.CheckboxItem>
<Menu.CheckboxItem checked={showSidebar} onCheckedChange={setShowSidebar} className={styles.CheckboxItem}>
<Menu.CheckboxItemIndicator className={styles.CheckboxItemIndicator}>
<CheckIcon className={styles.CheckboxItemIndicatorIcon} />
</Menu.CheckboxItemIndicator>
<span className={styles.CheckboxItemText}>Sidebar</span>
</Menu.CheckboxItem>
</Menu.Group>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className={styles.ArrowFill} />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className={styles.ArrowOuterStroke} />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className={styles.ArrowInnerStroke} />
</svg>
);
}
function ChevronDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...props}>
<path d="M1 3.5L5 7.5L9 3.5" stroke="currentcolor" strokeWidth="1.5" />
</svg>
);
}
function CheckIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="10" height="10" viewBox="0 0 10 10" {...props}>
<path d="M9.1603 1.12218C9.50684 1.34873 9.60427 1.81354 9.37792 2.16038L5.13603 8.66012C5.01614 8.8438 4.82192 8.96576 4.60451 8.99384C4.3871 9.02194 4.1683 8.95335 4.00574 8.80615L1.24664 6.30769C0.939709 6.02975 0.916013 5.55541 1.19372 5.24822C1.47142 4.94102 1.94536 4.91731 2.2523 5.19524L4.36085 7.10461L8.12299 1.33999C8.34934 0.993152 8.81376 0.895638 9.1603 1.12218Z" />
</svg>
);
}
```
### Nested menu
To create a submenu, nest another menu inside the parent menu with `<Menu.SubmenuRoot>`. Use the `<Menu.SubmenuTrigger>` part for the menu item that opens the nested menu.
```jsx {11-21} {12}#strong title="Adding a submenu"
<Menu.Root>
<Menu.Trigger />
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup>
<Menu.Arrow />
<Menu.Item />
{/* Submenu */}
<Menu.SubmenuRoot>
<Menu.SubmenuTrigger />
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup>
{/* prettier-ignore */}
{/* Submenu items */}
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.SubmenuRoot>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
```
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Menu } from "@base-ui/react/menu";
export default function ExampleMenu() {
return (
<Menu.Root>
<Menu.Trigger className="flex h-10 items-center justify-center gap-1.5 rounded-md border border-gray-200 bg-gray-50 px-3.5 text-base font-medium text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100 data-[popup-open]:bg-gray-100">
Song <ChevronDownIcon className="-mr-1" />
</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner className="outline-none" sideOffset={8}>
<Menu.Popup className="origin-[var(--transform-origin)] rounded-md bg-[canvas] py-1 text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0 dark:shadow-none dark:-outline-offset-1 dark:outline-gray-300">
<Menu.Arrow className="data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180">
<ArrowSvg />
</Menu.Arrow>
<Menu.Item className="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 data-[popup-open]:relative data-[popup-open]:z-0 data-[popup-open]:before:absolute data-[popup-open]:before:inset-x-1 data-[popup-open]:before:inset-y-0 data-[popup-open]:before:z-[-1] data-[popup-open]:before:rounded-sm data-[popup-open]:before:bg-gray-100 data-[highlighted]:data-[popup-open]:before:bg-gray-900">Add to Library</Menu.Item>
<Menu.SubmenuRoot>
<Menu.SubmenuTrigger className="flex cursor-default items-center justify-between gap-4 py-2 pr-4 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 data-[popup-open]:relative data-[popup-open]:z-0 data-[popup-open]:before:absolute data-[popup-open]:before:inset-x-1 data-[popup-open]:before:inset-y-0 data-[popup-open]:before:z-[-1] data-[popup-open]:before:rounded-sm data-[popup-open]:before:bg-gray-100 data-[highlighted]:data-[popup-open]:before:bg-gray-900">
Add to Playlist <ChevronRightIcon />
</Menu.SubmenuTrigger>
<Menu.Portal>
<Menu.Positioner className="outline-none" sideOffset={getOffset} alignOffset={getOffset}>
<Menu.Popup className="origin-[var(--transform-origin)] rounded-md bg-[canvas] py-1 text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0 dark:shadow-none dark:-outline-offset-1 dark:outline-gray-300">
<Menu.Item className="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 data-[popup-open]:relative data-[popup-open]:z-0 data-[popup-open]:before:absolute data-[popup-open]:before:inset-x-1 data-[popup-open]:before:inset-y-0 data-[popup-open]:before:z-[-1] data-[popup-open]:before:rounded-sm data-[popup-open]:before:bg-gray-100 data-[highlighted]:data-[popup-open]:before:bg-gray-900">Add to Library</Menu.Item>
<Menu.Item className="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 data-[popup-open]:relative data-[popup-open]:z-0 data-[popup-open]:before:absolute data-[popup-open]:before:inset-x-1 data-[popup-open]:before:inset-y-0 data-[popup-open]:before:z-[-1] data-[popup-open]:before:rounded-sm data-[popup-open]:before:bg-gray-100 data-[highlighted]:data-[popup-open]:before:bg-gray-900">Add to Playlist</Menu.Item>
<Menu.Separator className="mx-4 my-1.5 h-px bg-gray-200" />
<Menu.Item className="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 data-[popup-open]:relative data-[popup-open]:z-0 data-[popup-open]:before:absolute data-[popup-open]:before:inset-x-1 data-[popup-open]:before:inset-y-0 data-[popup-open]:before:z-[-1] data-[popup-open]:before:rounded-sm data-[popup-open]:before:bg-gray-100 data-[highlighted]:data-[popup-open]:before:bg-gray-900">Play Next</Menu.Item>
<Menu.Item className="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 data-[popup-open]:relative data-[popup-open]:z-0 data-[popup-open]:before:absolute data-[popup-open]:before:inset-x-1 data-[popup-open]:before:inset-y-0 data-[popup-open]:before:z-[-1] data-[popup-open]:before:rounded-sm data-[popup-open]:before:bg-gray-100 data-[highlighted]:data-[popup-open]:before:bg-gray-900">Play Last</Menu.Item>
<Menu.Separator className="mx-4 my-1.5 h-px bg-gray-200" />
<Menu.Item className="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 data-[popup-open]:relative data-[popup-open]:z-0 data-[popup-open]:before:absolute data-[popup-open]:before:inset-x-1 data-[popup-open]:before:inset-y-0 data-[popup-open]:before:z-[-1] data-[popup-open]:before:rounded-sm data-[popup-open]:before:bg-gray-100 data-[highlighted]:data-[popup-open]:before:bg-gray-900">Favorite</Menu.Item>
<Menu.Item className="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 data-[popup-open]:relative data-[popup-open]:z-0 data-[popup-open]:before:absolute data-[popup-open]:before:inset-x-1 data-[popup-open]:before:inset-y-0 data-[popup-open]:before:z-[-1] data-[popup-open]:before:rounded-sm data-[popup-open]:before:bg-gray-100 data-[highlighted]:data-[popup-open]:before:bg-gray-900">Share</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.SubmenuRoot>
<Menu.Separator className="mx-4 my-1.5 h-px bg-gray-200" />
<Menu.Item className="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 data-[popup-open]:relative data-[popup-open]:z-0 data-[popup-open]:before:absolute data-[popup-open]:before:inset-x-1 data-[popup-open]:before:inset-y-0 data-[popup-open]:before:z-[-1] data-[popup-open]:before:rounded-sm data-[popup-open]:before:bg-gray-100 data-[highlighted]:data-[popup-open]:before:bg-gray-900">Play Next</Menu.Item>
<Menu.Item className="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 data-[popup-open]:relative data-[popup-open]:z-0 data-[popup-open]:before:absolute data-[popup-open]:before:inset-x-1 data-[popup-open]:before:inset-y-0 data-[popup-open]:before:z-[-1] data-[popup-open]:before:rounded-sm data-[popup-open]:before:bg-gray-100 data-[highlighted]:data-[popup-open]:before:bg-gray-900">Play Last</Menu.Item>
<Menu.Separator className="mx-4 my-1.5 h-px bg-gray-200" />
<Menu.Item className="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 data-[popup-open]:relative data-[popup-open]:z-0 data-[popup-open]:before:absolute data-[popup-open]:before:inset-x-1 data-[popup-open]:before:inset-y-0 data-[popup-open]:before:z-[-1] data-[popup-open]:before:rounded-sm data-[popup-open]:before:bg-gray-100 data-[highlighted]:data-[popup-open]:before:bg-gray-900">Favorite</Menu.Item>
<Menu.Item className="flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 data-[popup-open]:relative data-[popup-open]:z-0 data-[popup-open]:before:absolute data-[popup-open]:before:inset-x-1 data-[popup-open]:before:inset-y-0 data-[popup-open]:before:z-[-1] data-[popup-open]:before:rounded-sm data-[popup-open]:before:bg-gray-100 data-[highlighted]:data-[popup-open]:before:bg-gray-900">Share</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
);
}
function getOffset({ side }: { side: Menu.Positioner.Props["side"] }) {
return side === "top" || side === "bottom" ? 4 : -4;
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className="fill-[canvas]" />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className="fill-gray-200 dark:fill-none" />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className="dark:fill-gray-300" />
</svg>
);
}
function ChevronDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...props}>
<path d="M1 3.5L5 7.5L9 3.5" stroke="currentcolor" strokeWidth="1.5" />
</svg>
);
}
function ChevronRightIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...props}>
<path d="M3.5 9L7.5 5L3.5 1" stroke="currentcolor" strokeWidth="1.5" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.ButtonIcon {
margin-right: -0.25rem;
}
.Positioner {
outline: 0;
}
.Popup {
box-sizing: border-box;
padding-block: 0.25rem;
border-radius: 0.375rem;
background-color: canvas;
color: var(--color-gray-900);
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.Item,
.SubmenuTrigger {
outline: 0;
cursor: default;
user-select: none;
padding-block: 0.5rem;
padding-left: 1rem;
padding-right: 2rem;
display: flex;
font-size: 0.875rem;
line-height: 1rem;
&[data-popup-open] {
z-index: 0;
position: relative;
}
&[data-popup-open]::before {
content: "";
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0.25rem;
border-radius: 0.25rem;
background-color: var(--color-gray-100);
}
&[data-highlighted] {
z-index: 0;
position: relative;
color: var(--color-gray-50);
}
&[data-highlighted]::before {
content: "";
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0.25rem;
border-radius: 0.25rem;
background-color: var(--color-gray-900);
}
}
.SubmenuTrigger {
align-items: center;
justify-content: space-between;
gap: 1rem;
padding-right: 1rem;
}
.Separator {
margin: 0.375rem 1rem;
height: 1px;
background-color: var(--color-gray-200);
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Menu } from "@base-ui/react/menu";
import styles from "./index.module.css";
export default function ExampleMenu() {
return (
<Menu.Root>
<Menu.Trigger className={styles.Button}>
Song <ChevronDownIcon className={styles.ButtonIcon} />
</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner className={styles.Positioner} sideOffset={8}>
<Menu.Popup className={styles.Popup}>
<Menu.Arrow className={styles.Arrow}>
<ArrowSvg />
</Menu.Arrow>
<Menu.Item className={styles.Item}>Add to Library</Menu.Item>
<Menu.SubmenuRoot>
<Menu.SubmenuTrigger className={styles.SubmenuTrigger}>
Add to Playlist
<ChevronRightIcon />
</Menu.SubmenuTrigger>
<Menu.Portal>
<Menu.Positioner className={styles.Positioner} sideOffset={getOffset} alignOffset={getOffset}>
<Menu.Popup className={styles.Popup}>
<Menu.Item className={styles.Item}>Get Up!</Menu.Item>
<Menu.Item className={styles.Item}>Inside Out</Menu.Item>
<Menu.Item className={styles.Item}>Night Beats</Menu.Item>
<Menu.Separator className={styles.Separator} />
<Menu.Item className={styles.Item}>New playlist…</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.SubmenuRoot>
<Menu.Separator className={styles.Separator} />
<Menu.Item className={styles.Item}>Play Next</Menu.Item>
<Menu.Item className={styles.Item}>Play Last</Menu.Item>
<Menu.Separator className={styles.Separator} />
<Menu.Item className={styles.Item}>Favorite</Menu.Item>
<Menu.Item className={styles.Item}>Share</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
);
}
function getOffset({ side }: { side: Menu.Positioner.Props["side"] }) {
return side === "top" || side === "bottom" ? 4 : -4;
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className={styles.ArrowFill} />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className={styles.ArrowOuterStroke} />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className={styles.ArrowInnerStroke} />
</svg>
);
}
function ChevronDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...props}>
<path d="M1 3.5L5 7.5L9 3.5" stroke="currentcolor" strokeWidth="1.5" />
</svg>
);
}
function ChevronRightIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...props}>
<path d="M3.5 9L7.5 5L3.5 1" stroke="currentcolor" strokeWidth="1.5" />
</svg>
);
}
```
### Navigate to another page
Use the `render` prop to compose a menu item with an anchor element.
```jsx title="A menu item that opens a link"
<Menu.Item render={<a href="/projects">Go to Projects</a>} />
```
### Open a dialog
In order to open a dialog using a menu, control the dialog state and open it imperatively using the `onClick` handler on the menu item.
```tsx {12-13,17-18,24-25,28-29} title="Connecting a dialog to a menu"
import * as React from "react";
import { Dialog } from "@base-ui/react/dialog";
import { Menu } from "@base-ui/react/menu";
function ExampleMenu() {
const [dialogOpen, setDialogOpen] = React.useState(false);
return (
<React.Fragment>
<Menu.Root>
<Menu.Trigger>Open menu</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup>
{/* Open the dialog when the menu item is clicked */}
<Menu.Item onClick={() => setDialogOpen(true)}>Open dialog</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
{/* Control the dialog state */}
<Dialog.Root open={dialogOpen} onOpenChange={setDialogOpen}>
<Dialog.Portal>
<Dialog.Backdrop />
<Dialog.Popup>
{/* prettier-ignore */}
{/* Rest of the dialog */}
</Dialog.Popup>
</Dialog.Portal>
</Dialog.Root>
</React.Fragment>
);
}
```
### Detached triggers
A menu can be opened by a trigger that lives either inside or outside the `<Menu.Root>`.
Keep the trigger inside `<Menu.Root>` for simple, tightly coupled layouts like the hero demo at the top of this page.
When the trigger and menu content need to live in different parts of the tree (for example, in a card list that controls a menu rendered near the document root), create a `handle` with `Menu.createHandle()` and pass it to both the trigger and the root.
Note that only top-level menus can have detached triggers.
Submenus must have their triggers defined within the `SubmenuRoot` part.
```jsx title="Detached triggers" {3,7} "handle={demoMenu}"
const demoMenu = Menu.createHandle();
<Menu.Trigger handle={demoMenu}>
Actions
</Menu.Trigger>
<Menu.Root handle={demoMenu}>
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup>
<Menu.Item>Edit</Menu.Item>
<Menu.Item>Share</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
```
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Menu } from "@base-ui/react/menu";
const demoMenu = Menu.createHandle();
const popupClass = "origin-[var(--transform-origin)] rounded-md bg-[canvas] py-1 text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0 dark:shadow-none dark:-outline-offset-1 dark:outline-gray-300";
const itemClass = "flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900";
export default function MenuDetachedTriggersSimpleDemo() {
return (
<React.Fragment>
<Menu.Trigger handle={demoMenu} aria-label="Project actions" className="flex size-10 items-center justify-center rounded-md border border-gray-200 bg-gray-50 text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100 data-[popup-open]:bg-gray-100 dark:border-gray-300 dark:bg-gray-100 dark:text-gray-900">
<DotsIcon />
</Menu.Trigger>
<Menu.Root handle={demoMenu}>
<Menu.Portal>
<Menu.Positioner sideOffset={8} className="outline-none">
<Menu.Popup className={popupClass}>
<Menu.Arrow className="data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180 text-gray-200 dark:text-gray-300">
<ArrowSvg />
</Menu.Arrow>
<Menu.Item className={itemClass}>Rename</Menu.Item>
<Menu.Item className={itemClass}>Duplicate</Menu.Item>
<Menu.Item className={itemClass}>Move to folder</Menu.Item>
<Menu.Separator className="mx-4 my-1.5 h-px bg-gray-200" />
<Menu.Item className={itemClass}>Archive</Menu.Item>
<Menu.Item className={`${itemClass} text-red-600`}>Delete</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
</React.Fragment>
);
}
export function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" fill="currentColor" opacity="0.1" />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" fill="currentColor" opacity="0.2" />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" fill="currentColor" />
</svg>
);
}
export function DotsIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" {...props}>
<circle cx="5" cy="12" r="1" />
<circle cx="12" cy="12" r="1" />
<circle cx="19" cy="12" r="1" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.IconButton {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
padding: 0;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Icon {
width: 1.25rem;
height: 1.25rem;
}
.Container {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.Button {
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Positioner {
outline: 0;
}
.Popup {
box-sizing: border-box;
padding-block: 0.25rem;
border-radius: 0.375rem;
background-color: canvas;
color: var(--color-gray-900);
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.Item {
outline: 0;
cursor: default;
user-select: none;
padding-block: 0.5rem;
padding-left: 1rem;
padding-right: 2rem;
display: flex;
font-size: 0.875rem;
line-height: 1rem;
color: inherit;
&[data-highlighted] {
z-index: 0;
position: relative;
color: var(--color-gray-50);
}
&[data-highlighted]::before {
content: "";
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0.25rem;
border-radius: 0.25rem;
background-color: var(--color-gray-900);
}
}
.Label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-gray-500);
padding: 0.5rem 1rem;
}
.Separator {
margin: 0.375rem 1rem;
height: 1px;
background-color: var(--color-gray-200);
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Menu } from "@base-ui/react/menu";
import styles from "./index.module.css";
const demoMenu = Menu.createHandle();
export default function MenuDetachedTriggersSimpleDemo() {
return (
<React.Fragment>
<Menu.Trigger className={styles.IconButton} handle={demoMenu} aria-label="Project actions">
<DotsIcon className={styles.Icon} />
</Menu.Trigger>
<Menu.Root handle={demoMenu}>
<Menu.Portal>
<Menu.Positioner sideOffset={8} className={styles.Positioner}>
<Menu.Popup className={styles.Popup}>
<Menu.Arrow className={styles.Arrow}>
<ArrowSvg />
</Menu.Arrow>
<Menu.Item className={styles.Item}>Rename</Menu.Item>
<Menu.Item className={styles.Item}>Duplicate</Menu.Item>
<Menu.Item className={styles.Item}>Move to folder</Menu.Item>
<Menu.Separator className={styles.Separator} />
<Menu.Item className={styles.Item}>Archive</Menu.Item>
<Menu.Item className={styles.Item}>Delete</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
</React.Fragment>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className={styles.ArrowFill} />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className={styles.ArrowOuterStroke} />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className={styles.ArrowInnerStroke} />
</svg>
);
}
function DotsIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" {...props}>
<circle cx="5" cy="12" r="1" />
<circle cx="12" cy="12" r="1" />
<circle cx="19" cy="12" r="1" />
</svg>
);
}
```
### Multiple triggers
One menu can be opened by several triggers.
You can either render multiple `<Menu.Trigger>` components inside the same `<Menu.Root>`, or attach several detached triggers to the same `handle`.
```jsx title="Multiple triggers within the Root part"
<Menu.Root>
<Menu.Trigger>Row actions</Menu.Trigger>
<Menu.Trigger>Quick actions</Menu.Trigger>
{/* Rest of the menu */}
</Menu.Root>
```
```jsx title="Multiple detached triggers"
const projectMenu = Menu.createHandle();
<Menu.Trigger handle={projectMenu}>Row actions</Menu.Trigger>
<Menu.Trigger handle={projectMenu}>Quick actions</Menu.Trigger>
<Menu.Root handle={projectMenu}>
{/* Rest of the menu */}
</Menu.Root>
```
Menus can render different content depending on which trigger opened them.
Pass a `payload` prop to each `<Menu.Trigger>` and read it via a function child on `<Menu.Root>`.
Provide a type argument to `createHandle()` to strongly type the payload.
```jsx title="Detached triggers with payload" {6,8,12,17}
const menus = {
file: ['New', 'Open', 'Save'],
edit: ['Undo', 'Redo', 'Cut', 'Copy', 'Paste'],
}
const demoMenu = Menu.createHandle<{ items: string[] }>();
<Menu.Trigger handle={demoMenu} payload={{ items: menus.file }}>
File
</Menu.Trigger>
<Menu.Trigger handle={demoMenu} payload={{ items: menus.edit }}>
Edit
</Menu.Trigger>
<Menu.Root handle={demoMenu}>
{({ payload }) => (
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup>
{(payload?.items ?? []).map((item) => (
<Menu.Item key={item}>{item}</Menu.Item>
))}
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
)}
</Menu.Root>
```
### Controlled mode with multiple triggers
Control a menu's open state externally with the `open` and `onOpenChange` props on `<Menu.Root>`.
When more than one trigger can open the menu, track the active trigger with the `triggerId` prop on `<Menu.Root>` and matching `id` props on each `<Menu.Trigger>`.
The `onOpenChange` callback receives `eventDetails`, which includes the DOM element that initiated the change, so you can update your `triggerId` state when the user activates a different trigger.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Menu } from "@base-ui/react/menu";
const itemClass = "flex cursor-default py-2 pr-8 pl-4 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900";
interface MenuItemDefinition {
label: string;
onClick?: () => void;
}
/* eslint-disable no-console */
const MENUS = {
library: [
{ label: "Add to library", onClick: () => console.log("Adding to library") },
{ label: "Add to favorites", onClick: () => console.log("Adding to favorites") },
] as MenuItemDefinition[],
playback: [
{ label: "Play", onClick: () => console.log("Playing") },
{ label: "Add to queue", onClick: () => console.log("Adding to queue") },
] as MenuItemDefinition[],
share: [
{ label: "Share", onClick: () => console.log("Sharing") },
{ label: "Copy link", onClick: () => console.log("Copying") },
] as MenuItemDefinition[],
};
/* eslint-enable no-console */
type MenuKey = keyof typeof MENUS;
const demoMenu = Menu.createHandle<MenuKey>();
export default function MenuDetachedTriggersControlledDemo() {
const [open, setOpen] = React.useState(false);
const [activeTrigger, setActiveTrigger] = React.useState<string | null>(null);
const handleOpenChange = (isOpen: boolean, eventDetails: Menu.Root.ChangeEventDetails) => {
setOpen(isOpen);
if (isOpen) {
setActiveTrigger(eventDetails.trigger?.id ?? null);
}
};
return (
<React.Fragment>
<div className="flex flex-wrap items-center gap-2">
<Menu.Trigger handle={demoMenu} payload={"library" as const} id="menu-trigger-1" className="flex h-10 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3.5 text-base font-medium text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100">
Library
</Menu.Trigger>
<Menu.Trigger handle={demoMenu} payload={"playback" as const} id="menu-trigger-2" className="flex h-10 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3.5 text-base font-medium text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100">
Playback
</Menu.Trigger>
<Menu.Trigger handle={demoMenu} payload={"share" as const} id="menu-trigger-3" className="flex h-10 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3.5 text-base font-medium text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100">
Share
</Menu.Trigger>
<button
type="button"
className="flex h-10 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3.5 text-base font-medium text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100"
onClick={() => {
setActiveTrigger("menu-trigger-2");
setOpen(true);
}}
>
Open playback (controlled)
</button>
</div>
<Menu.Root handle={demoMenu} open={open} triggerId={activeTrigger} onOpenChange={handleOpenChange}>
{({ payload }) => (
<Menu.Portal>
<Menu.Positioner sideOffset={8} className="outline-none">
<Menu.Popup className="origin-[var(--transform-origin)] rounded-md bg-[canvas] py-1 text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0 dark:shadow-none dark:-outline-offset-1 dark:outline-gray-300">
<Menu.Arrow className="data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180">
<ArrowSvg />
</Menu.Arrow>
{payload &&
MENUS[payload].map((item, index) => (
<Menu.Item key={index} className={itemClass} onClick={item.onClick}>
{item.label}
</Menu.Item>
))}
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
)}
</Menu.Root>
</React.Fragment>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className="fill-[canvas]" />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className="fill-gray-200 dark:fill-none" />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className="dark:fill-gray-300" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.IconButton {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
padding: 0;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Icon {
width: 1.25rem;
height: 1.25rem;
}
.Container {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.Button {
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Positioner {
outline: 0;
}
.Popup {
box-sizing: border-box;
padding-block: 0.25rem;
border-radius: 0.375rem;
background-color: canvas;
color: var(--color-gray-900);
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.Item {
outline: 0;
cursor: default;
user-select: none;
padding-block: 0.5rem;
padding-left: 1rem;
padding-right: 2rem;
display: flex;
font-size: 0.875rem;
line-height: 1rem;
color: inherit;
&[data-highlighted] {
z-index: 0;
position: relative;
color: var(--color-gray-50);
}
&[data-highlighted]::before {
content: "";
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0.25rem;
border-radius: 0.25rem;
background-color: var(--color-gray-900);
}
}
.Label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-gray-500);
padding: 0.5rem 1rem;
}
.Separator {
margin: 0.375rem 1rem;
height: 1px;
background-color: var(--color-gray-200);
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Menu } from "@base-ui/react/menu";
import styles from "./index.module.css";
/* eslint-disable no-console */
const itemGroups = {
library: [
{ label: "Add to library", onClick: () => console.log("Adding to library") },
{ label: "Add to favorites", onClick: () => console.log("Adding to favorites") },
],
playback: [
{ label: "Play", onClick: () => console.log("Playing") },
{ label: "Add to queue", onClick: () => console.log("Adding to queue") },
],
share: [
{ label: "Share", onClick: () => console.log("Sharing") },
{ label: "Copy link", onClick: () => console.log("Copying link") },
],
} as const;
/* eslint-enable no-console */
type MenuKey = keyof typeof itemGroups;
const demoMenu = Menu.createHandle<MenuKey>();
export default function MenuDetachedTriggersControlledDemo() {
const [open, setOpen] = React.useState(false);
const [activeTrigger, setActiveTrigger] = React.useState<string | null>(null);
const handleOpenChange = (isOpen: boolean, eventDetails: Menu.Root.ChangeEventDetails) => {
setOpen(isOpen);
if (isOpen) {
setActiveTrigger(eventDetails.trigger?.id ?? null);
}
};
return (
<React.Fragment>
<div className={styles.Container}>
<Menu.Trigger className={styles.Button} handle={demoMenu} id="menu-trigger-1" payload="library">
Library
</Menu.Trigger>
<Menu.Trigger className={styles.Button} handle={demoMenu} id="menu-trigger-2" payload="playback">
Playback
</Menu.Trigger>
<Menu.Trigger className={styles.Button} handle={demoMenu} id="menu-trigger-3" payload="share">
Share
</Menu.Trigger>
<button
type="button"
className={styles.Button}
onClick={() => {
setActiveTrigger("menu-trigger-2");
setOpen(true);
}}
>
Open playback (controlled)
</button>
</div>
<Menu.Root handle={demoMenu} open={open} triggerId={activeTrigger} onOpenChange={handleOpenChange}>
{({ payload }) => (
<Menu.Portal>
<Menu.Positioner className={styles.Positioner} sideOffset={8}>
<Menu.Popup className={styles.Popup}>
<Menu.Arrow className={styles.Arrow}>
<ArrowSvg />
</Menu.Arrow>
{payload &&
itemGroups[payload].map((item, index) => (
<Menu.Item key={index} className={styles.Item} onClick={item.onClick}>
{item.label}
</Menu.Item>
))}
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
)}
</Menu.Root>
</React.Fragment>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className={styles.ArrowFill} />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className={styles.ArrowOuterStroke} />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className={styles.ArrowInnerStroke} />
</svg>
);
}
```
### Filtering [Preview]
Filtering is a preview feature in `1.9.0`. Wrap `<Menu.Root>` in `<Menu.FilterProvider>`, add `<Menu.Input>` to the popup, and place the items inside `<Menu.List>`.
```jsx title="Filterable menu"
<Menu.FilterProvider>
<Menu.Root>
<Menu.Trigger>Actions</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup>
<Menu.Input aria-label="Filter actions" />
<Menu.Clear>Clear</Menu.Clear>
<Menu.Empty>No results.</Menu.Empty>
<Menu.List>
<Menu.Item>Rename</Menu.Item>
<Menu.Item>Delete</Menu.Item>
</Menu.List>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
</Menu.FilterProvider>
```
Label the input with `aria-label`, `aria-labelledby`, or a visible `<label>`. Typing narrows the actions, and the arrow keys move through the matching items.
#### Matching items
Items match their text by default. Use the `label` prop to override the text that is matched (and the text used for keyboard typeahead):
```jsx title="Labels"
<Menu.Item label="Move to folder">Move</Menu.Item>
```
By default an item matches when its text contains the query, ignoring case, accents, and punctuation. Use `filter` on `<Menu.FilterProvider>` to replace the matching function. It receives each item's text and the trimmed query, and keeps the item when it returns `true`:
```jsx title="Custom matching"
const { startsWith } = Menu.useFilter();
<Menu.FilterProvider filter={startsWith}>
<Menu.Root>{/* menu parts */}</Menu.Root>
</Menu.FilterProvider>;
```
`Menu.useFilter()` returns locale-aware `contains`, `startsWith`, and `endsWith` helpers, so use it instead of hand-rolled `toLowerCase()` comparisons when the content may contain accents or non-ASCII characters.
Groups with no matching items get the `hidden` attribute and stay mounted. If your styles set `display` on a group, add a `[hidden]` rule so it still hides:
```css title="Hiding styled groups"
.Group {
display: flex;
}
.Group[hidden] {
display: none;
}
```
`<Menu.Empty>` renders its content when no items match, so it can hold the "no results" message.
For external filtering, pass `filter={null}` and render the results yourself, omitting empty groups:
```jsx title="External filtering"
const { contains } = Menu.useFilter();
const [query, setQuery] = React.useState('');
const results = actions.filter((action) => contains(action.label, query));
<Menu.FilterProvider filter={null} value={query} onValueChange={setQuery}>
<Menu.Root>{/* render `results` */}</Menu.Root>
</Menu.FilterProvider>;
```
#### Controlling the query
`value` and `onValueChange` on `<Menu.FilterProvider>` control the query; `defaultValue` sets the initial uncontrolled query.
The query resets to an empty string when the menu closes, and that reset is reported with the `'popup-close'` reason. Cancel that change to keep the query between openings:
```jsx title="Keeping the query after close"
<Menu.FilterProvider
onValueChange={(value, eventDetails) => {
if (eventDetails.reason === 'popup-close') {
eventDetails.cancel();
}
}}
>
<Menu.Root>{/* menu parts */}</Menu.Root>
</Menu.FilterProvider>;
```
#### Highlighting
Use `autoHighlight` to highlight the first match while filtering, or `autoHighlight="always"` to highlight it even when the query is empty. The highlighted item carries the `data-highlighted` attribute.
`onItemHighlighted` on `<Menu.Root>` reports each highlight change:
```jsx title="Reporting the highlighted item"
<Menu.Root
onItemHighlighted={(highlightedItem, eventDetails) => {
console.log(eventDetails.reason, eventDetails.label);
}}
>
{/* menu parts */}
</Menu.Root>;
```
The callback receives the highlighted item element, or `undefined` when nothing is highlighted, plus event details:
- `reason` is `'keyboard'` when the highlight changed through arrow-key navigation, `'pointer'` when the pointer moved over items, and `'none'` for everything else, such as automatic highlighting while filtering, the item list changing, or the popup opening or closing.
- `event` is the originating native event. Pointer highlights may carry a `MouseEvent` rather than a `PointerEvent`, and typing reports the reason `'none'`.
- `label` is the highlighted item's `label` prop, or its text content when the prop is not set.
#### Focus and keyboard
Opening the menu with a click or the keyboard focuses the input. Opening it on hover, touch, or with a pen does not, so the on-screen keyboard stays hidden on mobile.
The arrow keys move the highlight while the input keeps focus. <kbd>Tab</kbd> closes the menu, and <kbd>Shift</kbd>+<kbd>Tab</kbd> returns focus to the trigger. The popup is a `dialog` that holds the `searchbox` input and the `menu` list, so keep `Menu.Input` and `Menu.List` inside `Menu.Popup`.
#### Submenus
Wrap each searchable `<Menu.SubmenuRoot>` in its own `<Menu.FilterProvider>`. Submenus without a provider remain unfiltered.
```jsx title="Filterable submenu"
<Menu.List>
<Menu.Item>New file</Menu.Item>
<Menu.FilterProvider>
<Menu.SubmenuRoot>
<Menu.SubmenuTrigger>Move to folder</Menu.SubmenuTrigger>
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup>
<Menu.Input aria-label="Filter folders" />
<Menu.List>
<Menu.Item>Desktop</Menu.Item>
<Menu.Item>Projects</Menu.Item>
</Menu.List>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.SubmenuRoot>
</Menu.FilterProvider>
</Menu.List>
```
#### Filtering with detached triggers
Use the same `Menu.createHandle()` handle for the root and its detached trigger, and wrap the root in the provider:
```jsx title="Detached trigger"
const handle = Menu.createHandle();
<Menu.Trigger handle={handle}>Actions</Menu.Trigger>
<Menu.FilterProvider>
<Menu.Root handle={handle}>{/* menu parts */}</Menu.Root>
</Menu.FilterProvider>
```
## API reference
### FilterProvider [Preview]
Enables filtering for the menu or submenu it wraps.
Add `Menu.Input` to the popup and place its items in `Menu.List`.
Wrap each searchable submenu in its own provider.
Doesn’t render its own HTML element.
**FilterProvider Props:**
| Prop | Type | Default | Description |
| :------------- | :------------------------------------------------------------------------ | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| children | `ReactNode` | - | The `<Menu.Root>` or `<Menu.SubmenuRoot>` to make filterable. |
| filter | `((text: string, query: string) => boolean) \| null` | - | Filter function used to match items against the query.
Receives each item's `label` (or its rendered text) and the trimmed query, and keeps the item when it returns `true`.
By default, items match when they contain the query, ignoring case, accents, and punctuation.
Pass `null` when rendering filtered items yourself. |
| autoHighlight | `boolean \| 'always'` | `false` | Whether filtering highlights the first matching item automatically.
`true`: highlight it while the query is not empty.
`'always'`: highlight it even when the query is empty. |
| locale | `Intl.LocalesArgument` | - | The locale the default `filter` uses for string comparison.
Defaults to the user's runtime locale. |
| defaultValue | `string` | - | The uncontrolled filter query when the menu is initially rendered.
To render a controlled query, use the `value` prop instead. |
| value | `string` | - | The filter query. Use when controlled.
When the popup closes, `onValueChange` is called with an empty query.
The controlled value changes only when the consumer updates this prop. |
| onValueChange | `((value: string, eventDetails: Menu.FilterProvider.ChangeEventDetails) => void)` | - | Event handler called when the filter query changes.
The close reset is reported with the `'popup-close'` reason; cancel it to keep the query. |
### Root
Groups all parts of the menu.
Doesn’t render its own HTML element.
**Root Props:**
| Prop | Type | Default | Description |
| :--------------------------------------------------------------------- | :---------------------------------------------------------------------- | :----------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| defaultOpen | `boolean` | `false` | Whether the menu is initially open.To render a controlled menu, use the `open` prop instead. |
| open | `boolean` | - | Whether the menu is currently open. |
| onOpenChange | `((open: boolean, eventDetails: Menu.Root.ChangeEventDetails) => void)` | - | Event handler called when the menu is opened or closed. |
| highlightItemOnHover | `boolean` | `true` | Whether moving the pointer over items should highlight them.
Disabling this prop allows CSS `:hover` to be differentiated from the `:focus` (`data-highlighted`) state. |
| actionsRef | `RefObject<Menu.Root.Actions \| null>` | - | A ref to imperative actions.\* `unmount`: Ends the closing phase of the menu after an externally controlled closing animation finishes.
Call `preventUnmountOnClose()` in `onOpenChange` first, otherwise the menu completes closing on its own.
Whether it leaves the DOM is decided by `keepMounted` on the portal. |
| \* `close`: Closes the menu imperatively when called. |
| onItemHighlighted | `((highlightedItem: HTMLElement \| undefined, eventDetails: Menu.Root.HighlightEventDetails) => void)` | - | Callback fired when an item is highlighted or unhighlighted.
`eventDetails.reason` is `'keyboard'`, `'pointer'`, or `'none'`.
Pointer highlights may carry a `MouseEvent` rather than a `PointerEvent`.
`eventDetails.label` is the item's `label` prop or its text content. |
| closeParentOnEsc | `boolean` | `false` | When in a submenu, determines whether pressing the Escape key
closes the entire menu, or only the current child menu. |
| defaultTriggerId | `string \| null` | - | ID of the trigger that the popover is associated with.
This is useful in conjunction with the `defaultOpen` prop to create an initially open popover. |
| handle | `Menu.Handle<Payload>` | - | A handle to associate the menu with a trigger.
If specified, allows external triggers to control the menu's open state. |
| loopFocus | `boolean` | `true` | Whether to loop keyboard focus back to the first item
when the end of the list is reached while using the arrow keys. |
| modal | `boolean` | `true` | Determines if the menu enters a modal state when open.\* `true`: user interaction is limited to the menu: document page scroll is locked and pointer interactions on outside elements are disabled. |
| \* `false`: user interaction with the rest of the document is allowed. |
| onOpenChangeComplete | `((open: boolean) => void)` | - | Event handler called after any animations complete when the menu is closed. |
| triggerId | `string \| null` | - | ID of the trigger that the popover is associated with.
This is useful in conjunction with the `open` prop to create a controlled popover.
There's no need to specify this prop when the popover is uncontrolled (i.e. when the `open` prop is not set). |
| disabled | `boolean` | `false` | Whether the component should ignore user interaction. |
| orientation | `Menu.Root.Orientation` | `'vertical'` | The visual orientation of the menu.
Controls whether roving focus uses up/down or left/right arrow keys. |
| children | `ReactNode \| PayloadChildRenderFunction<Payload>` | - | The content of the popover.
This can be a regular React node or a render function that receives the `payload` of the active trigger. |
### Trigger
A button that opens the menu.
Renders a `<button>` element.
**Trigger Props:**
| Prop | Type | Default | Description |
| :----------- | :-------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| handle | `Menu.Handle<Payload>` | - | A handle to associate the trigger with a menu. |
| nativeButton | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it
via the `render` prop.
Set to `false` if the rendered element is not a button (e.g. `<div>`). |
| payload | `Payload` | - | A payload to pass to the menu when it is opened. |
| disabled | `boolean` | `false` | Whether the component should ignore user interaction. |
| openOnHover | `boolean` | - | Whether the menu should also open when the trigger is hovered. |
| delay | `number` | `100` | How long to wait before the menu may be opened on hover. Specified in milliseconds.Requires the `openOnHover` prop. |
| closeDelay | `number` | `0` | How long to wait before closing the menu that was opened on hover.
Specified in milliseconds.Requires the `openOnHover` prop. |
| children | `ReactNode` | - | - |
| className | `string \| ((state: Menu.Trigger.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Menu.Trigger.State) => CSSProperties \| undefined)` | - | \* |
| render | `ReactElement \| ((props: HTMLProps, state: Menu.Trigger.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Trigger Data Attributes:**
| Attribute | Type | Description |
| :-------------- | :--- | :------------------------------------------- |
| data-popup-open | - | Present when the corresponding menu is open. |
| data-pressed | - | Present when the trigger is pressed. |
### Portal
A portal element that moves the popup to a different part of the DOM.
By default, the portal element is appended to `<body>`.
Renders a `<div>` element.
**Portal Props:**
| Prop | Type | Default | Description |
| :---------- | :---------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| container | `HTMLElement \| ShadowRoot \| RefObject<HTMLElement \| ShadowRoot \| null> \| null` | - | A parent element to render the portal element into. |
| className | `string \| ((state: Menu.Portal.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Menu.Portal.State) => CSSProperties \| undefined)` | - | - |
| keepMounted | `boolean` | `false` | Whether to keep the portal mounted in the DOM while the popup is hidden. |
| render | `ReactElement \| ((props: HTMLProps, state: Menu.Portal.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Backdrop
An overlay displayed beneath the menu popup.
Renders a `<div>` element.
**Backdrop Props:**
| Prop | Type | Default | Description |
| :-------- | :--------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Menu.Backdrop.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Menu.Backdrop.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Menu.Backdrop.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Backdrop Data Attributes:**
| Attribute | Type | Description |
| :------------------ | :--- | :-------------------------------------- |
| data-open | - | Present when the menu is open. |
| data-closed | - | Present when the menu is closed. |
| data-starting-style | - | Present when the menu is animating in. |
| data-ending-style | - | Present when the menu is animating out. |
### Positioner
Positions the menu popup against the trigger.
Renders a `<div>` element.
**Positioner Props:**
| Prop | Type | Default | Description |
| :-------------------- | :------------------------- | :--------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| disableAnchorTracking | `boolean` | `false` | Whether to disable the popup from tracking any layout shift of its positioning anchor. |
| align | `Align` | `'center'` | How to align the popup relative to the specified side. |
| alignOffset | `number \| OffsetFunction` | `0` | Additional offset along the alignment axis in pixels.
Also accepts a function that returns the offset to read the dimensions of the anchor
and positioner elements, along with its side and alignment.The function takes a `data` object parameter with the following properties:\* `data.anchor`: the dimensions of the anchor element with properties `width` and `height`. |
- `data.positioner`: the dimensions of the positioner element with properties `width` and `height`.
- `data.side`: which side of the anchor element the positioner is aligned against.
- `data.align`: how the positioner is aligned relative to the specified side. |
| side | `Side` | `'bottom'` | Which side of the anchor element to align the popup against.
May automatically change to avoid collisions. |
| sideOffset | `number \| OffsetFunction` | `0` | Distance between the anchor and the popup in pixels.
Also accepts a function that returns the distance to read the dimensions of the anchor
and positioner elements, along with its side and alignment.The function takes a `data` object parameter with the following properties:\* `data.anchor`: the dimensions of the anchor element with properties `width` and `height`.
- `data.positioner`: the dimensions of the positioner element with properties `width` and `height`.
- `data.side`: which side of the anchor element the positioner is aligned against.
- `data.align`: how the positioner is aligned relative to the specified side. |
| arrowPadding | `number` | `5` | Minimum distance to maintain between the arrow and the edges of the popup.Use it to prevent the arrow element from hanging out of the rounded corners of a popup. |
| anchor | `Element \| RefObject<Element \| null> \| VirtualElement \| (() => Element \| VirtualElement \| null) \| null` | - | An element to position the popup against.
By default, the popup will be positioned against the trigger. |
| collisionAvoidance | `CollisionAvoidance` | - | Determines how to handle collisions when positioning the popup. |
| collisionBoundary | `Boundary` | `'clipping-ancestors'` | An element or a rectangle that delimits the area that the popup is confined to. |
| collisionPadding | `Padding` | `5` | Additional space to maintain from the edge of the collision boundary. |
| sticky | `boolean` | `false` | Whether to maintain the popup in the viewport after
the anchor element was scrolled out of view. |
| positionMethod | `'fixed' \| 'absolute'` | `'absolute'` | Determines which CSS `position` property to use. |
| className | `string \| ((state: Menu.Positioner.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Menu.Positioner.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Menu.Positioner.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Positioner Data Attributes:**
| Attribute | Type | Description |
| :----------------- | :------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| data-open | - | Present when the menu popup is open. |
| data-closed | - | Present when the menu popup is closed. |
| data-anchor-hidden | - | Present when the anchor is hidden. |
| data-align | `'start' \| 'center' \| 'end'` | Indicates how the popup is aligned relative to specified side. |
| data-side | `'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the popup is positioned relative to the trigger. |
**Positioner CSS Variables:**
| Variable | Type | Default | Description |
| :----------------- | :------- | :------ | :------------------------------------------------------------------------------------- |
| --anchor-height | `number` | - | The anchor's height. |
| --anchor-width | `number` | - | The anchor's width. |
| --available-height | `number` | - | The available height between the trigger and the edge of the viewport. |
| --available-width | `number` | - | The available width between the trigger and the edge of the viewport. |
| --transform-origin | `string` | - | The coordinates that this element is anchored to. Used for animations and transitions. |
### Popup
A container for the menu items.
Renders a `<div>` element.
**Popup Props:**
| Prop | Type | Default | Description |
| :--------- | :---------------------------------------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------- |
| finalFocus | `boolean \| RefObject<HTMLElement \| null> \| ((closeType: InteractionType) => boolean \| void \| HTMLElement \| null)` | - | Determines the element to focus when the menu is closed.\* `false`: Do not move focus. |
- `true`: Move focus based on the default behavior (trigger or previously focused element).
- `RefObject`: Move focus to the ref element.
- `function`: Called with the interaction type (`mouse`, `touch`, `pen`, or `keyboard`).
Return an element to focus, `true` to use the default behavior, or `false`/`undefined` to do nothing. |
| children | `ReactNode` | - | - |
| className | `string \| ((state: Menu.Popup.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Menu.Popup.State) => CSSProperties \| undefined)` | - | \* |
| render | `ReactElement \| ((props: HTMLProps, state: Menu.Popup.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Popup Data Attributes:**
| Attribute | Type | Description |
| :------------------ | :------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| data-open | - | Present when the menu is open. |
| data-closed | - | Present when the menu is closed. |
| data-align | `'start' \| 'center' \| 'end'` | Indicates how the popup is aligned relative to specified side. |
| data-instant | `'click' \| 'dismiss' \| 'group'` | Present if animations should be instant. |
| data-side | `'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the popup is positioned relative to the trigger. |
| data-starting-style | - | Present when the menu is animating in. |
| data-ending-style | - | Present when the menu is animating out. |
### Arrow
Displays an element positioned against the menu anchor.
Renders a `<div>` element.
**Arrow Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Menu.Arrow.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Menu.Arrow.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Menu.Arrow.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Arrow Data Attributes:**
| Attribute | Type | Description |
| :-------------- | :------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| data-open | - | Present when the menu popup is open. |
| data-closed | - | Present when the menu popup is closed. |
| data-uncentered | - | Present when the menu arrow is uncentered. |
| data-align | `'start' \| 'center' \| 'end'` | Indicates how the popup is aligned relative to specified side. |
| data-side | `'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the popup is positioned relative to the trigger. |
### Input
A search field that filters the menu items.
Requires the menu to be wrapped in `Menu.FilterProvider`.
Renders an `<input>` element.
**Input Props:**
| Prop | Type | Default | Description |
| :----------- | :------------------------------------------------------------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Menu.Input.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Menu.Input.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Menu.Input.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Input Data Attributes:**
| Attribute | Type | Description |
| :-------- | :--- | :---------------------------------------------- |
| data-highlighted | - | Present while the input has the focus ring. |
Label the input with `aria-label`, `aria-labelledby`, or a visible `<label>`.
### Clear
A button that clears the input text.
Requires the menu to be wrapped in `Menu.FilterProvider`.
Renders a `<button>` element when the input has text.
**Clear Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| disabled | `boolean` | `false` | Whether the component should ignore user interaction. |
| className | `string \| ((state: Menu.Clear.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Menu.Clear.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Menu.Clear.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Empty
A message shown when the menu has no matching items.
Requires the menu to be wrapped in `Menu.FilterProvider`.
Renders a `<div>` element with `role="status"` and `aria-live="polite"`, so the message is announced when it appears.
**Empty Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Menu.Empty.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Menu.Empty.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Menu.Empty.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### List
Groups menu items so other content, such as a filter input, can share the popup.
Renders a `<div>` element.
**List Props:**
| Prop | Type | Default | Description |
| :-------- | :---------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Menu.List.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Menu.List.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Menu.List.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
Groups inside the list that end up with no matching items get the `hidden` attribute and stay mounted.
### Item
An individual interactive item in the menu.
Renders a `<div>` element.
**Item Props:**
| Prop | Type | Default | Description |
| :----------- | :----------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| label | `string` | - | Overrides the text used for keyboard text navigation and filtering.
Falls back to the rendered text when not provided. |
| onClick | `MouseEventHandler<HTMLElement>` | - | The click handler for the menu item. |
| closeOnClick | `boolean` | `true` | Whether to close the menu when the item is clicked. |
| nativeButton | `boolean` | `false` | Whether the component renders a native `<button>` element when replacing it
via the `render` prop.
Set to `true` if the rendered element is a native button. |
| disabled | `boolean` | `false` | Whether the component should ignore user interaction. |
| className | `string \| ((state: Menu.Item.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Menu.Item.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Menu.Item.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Item Data Attributes:**
| Attribute | Type | Description |
| :--------------- | :--- | :----------------------------------------- |
| data-highlighted | - | Present when the menu item is highlighted. |
| data-disabled | - | Present when the menu item is disabled. |
### SubmenuRoot
Groups all parts of a submenu.
Doesn’t render its own HTML element.
**SubmenuRoot Props:**
| Prop | Type | Default | Description |
| :--------------------------------------------------------------- | :----------------------------------------------------------------------------- | :----------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| defaultOpen | `boolean` | `false` | Whether the menu is initially open.To render a controlled menu, use the `open` prop instead. |
| open | `boolean` | - | Whether the menu is currently open. |
| onOpenChange | `((open: boolean, eventDetails: Menu.SubmenuRoot.ChangeEventDetails) => void)` | - | Event handler called when the menu is opened or closed. |
| highlightItemOnHover | `boolean` | `true` | Whether moving the pointer over items should highlight them.
Disabling this prop allows CSS `:hover` to be differentiated from the `:focus` (`data-highlighted`) state. |
| actionsRef | `RefObject<Menu.Root.Actions \| null>` | - | A ref to imperative actions.\* `unmount`: Ends the closing phase of the submenu after an externally controlled closing animation finishes.
Call `preventUnmountOnClose()` in `onOpenChange` first, otherwise the submenu completes closing on its own.
Whether it leaves the DOM is decided by `keepMounted` on the portal. |
| \* `close`: Closes the submenu imperatively when called. |
| closeParentOnEsc | `boolean` | `false` | When in a submenu, determines whether pressing the Escape key
closes the entire menu, or only the current child menu. |
| defaultTriggerId | `string \| null` | - | ID of the trigger that the popover is associated with.
This is useful in conjunction with the `defaultOpen` prop to create an initially open popover. |
| handle | `Menu.Handle<unknown>` | - | A handle to associate the menu with a trigger.
If specified, allows external triggers to control the menu's open state. |
| loopFocus | `boolean` | `true` | Whether to loop keyboard focus back to the first item
when the end of the list is reached while using the arrow keys. |
| onOpenChangeComplete | `((open: boolean) => void)` | - | Event handler called after any animations complete when the menu is closed. |
| triggerId | `string \| null` | - | ID of the trigger that the popover is associated with.
This is useful in conjunction with the `open` prop to create a controlled popover.
There's no need to specify this prop when the popover is uncontrolled (i.e. when the `open` prop is not set). |
| disabled | `boolean` | `false` | Whether the component should ignore user interaction. |
| orientation | `Menu.Root.Orientation` | `'vertical'` | The visual orientation of the menu.
Controls whether roving focus uses up/down or left/right arrow keys. |
| children | `ReactNode \| PayloadChildRenderFunction<unknown>` | - | The content of the popover.
This can be a regular React node or a render function that receives the `payload` of the active trigger. |
### SubmenuTrigger
A menu item that opens a submenu.
Renders a `<div>` element.
**SubmenuTrigger Props:**
| Prop | Type | Default | Description |
| :----------- | :--------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| label | `string` | - | Overrides the text label to use when the item is matched during keyboard text navigation. |
| onClick | `MouseEventHandler<HTMLElement>` | - | - |
| nativeButton | `boolean` | `false` | Whether the component renders a native `<button>` element when replacing it
via the `render` prop.
Set to `true` if the rendered element is a native button. |
| disabled | `boolean` | `false` | Whether the component should ignore user interaction. |
| openOnHover | `boolean` | - | Whether the menu should also open when the trigger is hovered. |
| delay | `number` | `100` | How long to wait before the menu may be opened on hover. Specified in milliseconds.Requires the `openOnHover` prop. |
| closeDelay | `number` | `0` | How long to wait before closing the menu that was opened on hover.
Specified in milliseconds.Requires the `openOnHover` prop. |
| className | `string \| ((state: Menu.SubmenuTrigger.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Menu.SubmenuTrigger.State) => CSSProperties \| undefined)` | - | \* |
| render | `ReactElement \| ((props: HTMLProps, state: Menu.SubmenuTrigger.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**SubmenuTrigger Data Attributes:**
| Attribute | Type | Description |
| :--------------- | :--- | :----------------------------------------------- |
| data-popup-open | - | Present when the corresponding submenu is open. |
| data-highlighted | - | Present when the submenu trigger is highlighted. |
| data-disabled | - | Present when the submenu trigger is disabled. |
### Group
Groups related menu items with the corresponding label.
Renders a `<div>` element.
**Group Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| children | `ReactNode` | - | The content of the component. |
| className | `string \| ((state: Menu.Group.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Menu.Group.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Menu.Group.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### GroupLabel
An accessible label that is automatically associated with its parent group.
Renders a `<div>` element.
**GroupLabel Props:**
| Prop | Type | Default | Description |
| :-------- | :----------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Menu.GroupLabel.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Menu.GroupLabel.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Menu.GroupLabel.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### RadioGroup
Groups related radio items.
Renders a `<div>` element.
**RadioGroup Props:**
| Prop | Type | Default | Description |
| :------------ | :----------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| defaultValue | `any` | - | The uncontrolled value of the radio item that should be initially selected.To render a controlled radio group, use the `value` prop instead. |
| value | `any` | - | The controlled value of the radio item that should be currently selected.To render an uncontrolled radio group, use the `defaultValue` prop instead. |
| onValueChange | `((value: any, eventDetails: Menu.RadioGroup.ChangeEventDetails) => void)` | - | Function called when the selected value changes. |
| disabled | `boolean` | `false` | Whether the component should ignore user interaction. |
| children | `ReactNode` | - | The content of the component. |
| className | `string \| ((state: Menu.RadioGroup.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Menu.RadioGroup.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Menu.RadioGroup.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### RadioItem
A menu item that works like a radio button in a given group.
Renders a `<div>` element.
**RadioItem Props:**
| Prop | Type | Default | Description |
| :----------- | :---------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| label | `string` | - | Overrides the text label to use when the item is matched during keyboard text navigation. |
| value | `any` | - | Value of the radio item.
This is the value that will be set in the Menu.RadioGroup when the item is selected. |
| onClick | `MouseEventHandler<HTMLElement>` | - | The click handler for the menu item. |
| closeOnClick | `boolean` | `false` | Whether to close the menu when the item is clicked. |
| nativeButton | `boolean` | `false` | Whether the component renders a native `<button>` element when replacing it
via the `render` prop.
Set to `true` if the rendered element is a native button. |
| disabled | `boolean` | `false` | Whether the component should ignore user interaction. |
| className | `string \| ((state: Menu.RadioItem.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Menu.RadioItem.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Menu.RadioItem.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**RadioItem Data Attributes:**
| Attribute | Type | Description |
| :--------------- | :--- | :------------------------------------------------ |
| data-checked | - | Present when the menu radio item is selected. |
| data-unchecked | - | Present when the menu radio item is not selected. |
| data-highlighted | - | Present when the menu radio item is highlighted. |
| data-disabled | - | Present when the menu radio item is disabled. |
### RadioItemIndicator
Indicates whether the radio item is selected.
Renders a `<div>` element.
**RadioItemIndicator Props:**
| Prop | Type | Default | Description |
| :---------- | :------------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Menu.RadioItemIndicator.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Menu.RadioItemIndicator.State) => CSSProperties \| undefined)` | - | - |
| keepMounted | `boolean` | `false` | Whether to keep the HTML element in the DOM when the radio item is inactive. |
| render | `ReactElement \| ((props: HTMLProps, state: Menu.RadioItemIndicator.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**RadioItemIndicator Data Attributes:**
| Attribute | Type | Description |
| :------------------ | :--- | :------------------------------------------------- |
| data-checked | - | Present when the menu radio item is selected. |
| data-unchecked | - | Present when the menu radio item is not selected. |
| data-disabled | - | Present when the menu radio item is disabled. |
| data-starting-style | - | Present when the radio indicator is animating in. |
| data-ending-style | - | Present when the radio indicator is animating out. |
### CheckboxItem
A menu item that toggles a setting on or off.
Renders a `<div>` element.
**CheckboxItem Props:**
| Prop | Type | Default | Description |
| :-------------- | :------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| label | `string` | - | Overrides the text label to use when the item is matched during keyboard text navigation. |
| defaultChecked | `boolean` | `false` | Whether the checkbox item is initially ticked.To render a controlled checkbox item, use the `checked` prop instead. |
| checked | `boolean` | - | Whether the checkbox item is currently ticked.To render an uncontrolled checkbox item, use the `defaultChecked` prop instead. |
| onCheckedChange | `((checked: boolean, eventDetails: Menu.CheckboxItem.ChangeEventDetails) => void)` | - | Event handler called when the checkbox item is ticked or unticked. |
| onClick | `MouseEventHandler<HTMLElement>` | - | The click handler for the menu item. |
| closeOnClick | `boolean` | `false` | Whether to close the menu when the item is clicked. |
| nativeButton | `boolean` | `false` | Whether the component renders a native `<button>` element when replacing it
via the `render` prop.
Set to `true` if the rendered element is a native button. |
| disabled | `boolean` | `false` | Whether the component should ignore user interaction. |
| className | `string \| ((state: Menu.CheckboxItem.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Menu.CheckboxItem.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Menu.CheckboxItem.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**CheckboxItem Data Attributes:**
| Attribute | Type | Description |
| :--------------- | :--- | :-------------------------------------------------- |
| data-checked | - | Present when the menu checkbox item is checked. |
| data-unchecked | - | Present when the menu checkbox item is not checked. |
| data-highlighted | - | Present when the menu checkbox item is highlighted. |
| data-disabled | - | Present when the menu checkbox item is disabled. |
### CheckboxItemIndicator
Indicates whether the checkbox item is ticked.
Renders a `<div>` element.
**CheckboxItemIndicator Props:**
| Prop | Type | Default | Description |
| :---------- | :---------------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Menu.CheckboxItemIndicator.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Menu.CheckboxItemIndicator.State) => CSSProperties \| undefined)` | - | - |
| keepMounted | `boolean` | `false` | Whether to keep the HTML element in the DOM when the checkbox item is not checked. |
| render | `ReactElement \| ((props: HTMLProps, state: Menu.CheckboxItemIndicator.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**CheckboxItemIndicator Data Attributes:**
| Attribute | Type | Description |
| :------------------ | :--- | :-------------------------------------------------- |
| data-checked | - | Present when the menu checkbox item is checked. |
| data-unchecked | - | Present when the menu checkbox item is not checked. |
| data-disabled | - | Present when the menu checkbox item is disabled. |
| data-starting-style | - | Present when the indicator is animating in. |
| data-ending-style | - | Present when the indicator is animating out. |
### Separator
A separator element accessible to screen readers.
Renders a `<div>` element.
**Separator Props:**
| Prop | Type | Default | Description |
| :---------- | :----------------------------------------------------------------------------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| orientation | `Orientation` | `'horizontal'` | The orientation of the separator. |
| className | `string \| ((state: Separator.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Separator.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Separator.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### useFilter
When filtering items yourself, `Menu.useFilter()` matches text according to the user's locale.
It returns `contains`, `startsWith`, and `endsWith` helpers, each taking `(item, query, itemToString?)` and returning a boolean.
An empty query always matches.
```jsx title="Locale-aware matching"
const { contains } = Menu.useFilter();
const results = actions.filter((action) => contains(action.label, query));
```
Pass the same helpers to `<Menu.FilterProvider filter={...}>` when you want the built-in filtering to use them.
references/menubar.md
---
title: Menubar
subtitle: A menu bar providing commands and options for your application.
description: A menu bar providing commands and options for your application.
---
# Menubar
A menu bar providing commands and options for your application.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Menubar } from "@base-ui/react/menubar";
import { Menu } from "@base-ui/react/menu";
export default function ExampleMenubar() {
return (
<Menubar className="flex rounded-md border border-gray-200 bg-gray-50 p-0.5">
<Menu.Root>
<Menu.Trigger className="h-8 rounded px-3 text-sm font-medium text-gray-600 outline-none select-none focus-visible:bg-gray-100 data-[disabled]:opacity-50 data-[popup-open]:bg-gray-100">File</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner className="outline-none" sideOffset={6}>
<Menu.Popup className="origin-[var(--transform-origin)] rounded-md bg-[canvas] py-1 text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 data-[ending-style]:opacity-0 data-[ending-style]:transition-opacity data-[instant]:transition-none dark:shadow-none dark:outline dark:outline-1 dark:-outline-offset-1 dark:outline-gray-300">
<Menu.Item onClick={handleClick} className="flex cursor-default items-center justify-between gap-4 px-4 py-2 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
New
</Menu.Item>
<Menu.Item onClick={handleClick} className="flex cursor-default items-center justify-between gap-4 px-4 py-2 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
Open
</Menu.Item>
<Menu.Item onClick={handleClick} className="flex cursor-default items-center justify-between gap-4 px-4 py-2 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
Save
</Menu.Item>
<Menu.SubmenuRoot>
<Menu.SubmenuTrigger className="flex w-full cursor-default items-center justify-between gap-4 px-4 py-2 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 data-[popup-open]:relative data-[popup-open]:z-0 data-[popup-open]:before:absolute data-[popup-open]:before:inset-x-1 data-[popup-open]:before:inset-y-0 data-[popup-open]:before:z-[-1] data-[popup-open]:before:rounded-sm data-[popup-open]:before:bg-gray-100 data-[highlighted]:data-[popup-open]:before:bg-gray-900">
Export
<ChevronRightIcon />
</Menu.SubmenuTrigger>
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup className="origin-[var(--transform-origin)] rounded-md bg-[canvas] py-1 text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 data-[ending-style]:opacity-0 data-[ending-style]:transition-opacity data-[instant]:transition-none dark:shadow-none dark:outline dark:outline-1 dark:-outline-offset-1 dark:outline-gray-300">
<Menu.Item onClick={handleClick} className="flex cursor-default items-center justify-between gap-4 px-4 py-2 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
PDF
</Menu.Item>
<Menu.Item onClick={handleClick} className="flex cursor-default items-center justify-between gap-4 px-4 py-2 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
PNG
</Menu.Item>
<Menu.Item onClick={handleClick} className="flex cursor-default items-center justify-between gap-4 px-4 py-2 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
SVG
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.SubmenuRoot>
<Menu.Separator className="mx-4 my-1.5 h-px bg-gray-200" />
<Menu.Item onClick={handleClick} className="flex cursor-default items-center justify-between gap-4 px-4 py-2 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
Print
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
<Menu.Root>
<Menu.Trigger className="h-8 rounded px-3 text-sm font-medium text-gray-600 outline-none select-none focus-visible:bg-gray-100 data-[disabled]:opacity-50 data-[popup-open]:bg-gray-100">Edit</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner className="outline-none" sideOffset={6}>
<Menu.Popup className="origin-[var(--transform-origin)] rounded-md bg-[canvas] py-1 text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 data-[ending-style]:opacity-0 data-[ending-style]:transition-opacity data-[instant]:transition-none dark:shadow-none dark:outline dark:outline-1 dark:-outline-offset-1 dark:outline-gray-300">
<Menu.Item onClick={handleClick} className="flex cursor-default items-center justify-between gap-4 px-4 py-2 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
Cut
</Menu.Item>
<Menu.Item onClick={handleClick} className="flex cursor-default items-center justify-between gap-4 px-4 py-2 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
Copy
</Menu.Item>
<Menu.Item onClick={handleClick} className="flex cursor-default items-center justify-between gap-4 px-4 py-2 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
Paste
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
<Menu.Root>
<Menu.Trigger className="h-8 rounded px-3 text-sm font-medium text-gray-600 outline-none select-none focus-visible:bg-gray-100 data-[disabled]:opacity-50 data-[popup-open]:bg-gray-100">View</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner className="outline-none" sideOffset={6}>
<Menu.Popup className="origin-[var(--transform-origin)] rounded-md bg-[canvas] py-1 text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 data-[ending-style]:opacity-0 data-[ending-style]:transition-opacity data-[instant]:transition-none dark:shadow-none dark:outline dark:outline-1 dark:-outline-offset-1 dark:outline-gray-300">
<Menu.Item onClick={handleClick} className="flex cursor-default items-center justify-between gap-4 px-4 py-2 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
Zoom In
</Menu.Item>
<Menu.Item onClick={handleClick} className="flex cursor-default items-center justify-between gap-4 px-4 py-2 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
Zoom Out
</Menu.Item>
<Menu.SubmenuRoot>
<Menu.SubmenuTrigger className="flex w-full cursor-default items-center justify-between gap-4 px-4 py-2 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 data-[popup-open]:relative data-[popup-open]:z-0 data-[popup-open]:before:absolute data-[popup-open]:before:inset-x-1 data-[popup-open]:before:inset-y-0 data-[popup-open]:before:z-[-1] data-[popup-open]:before:rounded-sm data-[popup-open]:before:bg-gray-100 data-[highlighted]:data-[popup-open]:before:bg-gray-900">
Layout
<ChevronRightIcon />
</Menu.SubmenuTrigger>
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup className="origin-[var(--transform-origin)] rounded-md bg-[canvas] py-1 text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 data-[ending-style]:opacity-0 data-[ending-style]:transition-opacity data-[instant]:transition-none dark:shadow-none dark:outline dark:outline-1 dark:-outline-offset-1 dark:outline-gray-300">
<Menu.Item onClick={handleClick} className="flex cursor-default items-center justify-between gap-4 px-4 py-2 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
Single Page
</Menu.Item>
<Menu.Item onClick={handleClick} className="flex cursor-default items-center justify-between gap-4 px-4 py-2 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
Two Pages
</Menu.Item>
<Menu.Item onClick={handleClick} className="flex cursor-default items-center justify-between gap-4 px-4 py-2 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
Continuous
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.SubmenuRoot>
<Menu.Separator className="mx-4 my-1.5 h-px bg-gray-200" />
<Menu.Item onClick={handleClick} className="flex cursor-default items-center justify-between gap-4 px-4 py-2 text-sm leading-4 outline-none select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900">
Full Screen
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
<Menu.Root disabled>
<Menu.Trigger className="h-8 rounded px-3 text-sm font-medium text-gray-600 outline-none select-none focus-visible:bg-gray-100 data-[disabled]:opacity-50 data-[popup-open]:bg-gray-100">Help</Menu.Trigger>
</Menu.Root>
</Menubar>
);
}
function handleClick(event: React.MouseEvent<HTMLElement>) {
// eslint-disable-next-line no-console
console.log(`${event.currentTarget.textContent} clicked`);
}
function ChevronRightIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" {...props}>
<path d="M6 12L10 8L6 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Menubar {
display: flex;
background-color: var(--color-gray-50);
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
padding: 0.125rem;
}
.MenuTrigger {
box-sizing: border-box;
background: none;
padding: 0 0.75rem;
margin: 0;
outline: 0;
border: 0;
color: var(--color-gray-600);
border-radius: 0.25rem;
user-select: none;
height: 2rem;
font-family: inherit;
font-size: 0.875rem;
font-weight: 500;
&[data-pressed],
&:focus-visible {
background-color: var(--color-gray-100);
outline: none;
}
&[data-disabled] {
opacity: 0.5;
}
}
.MenuPositioner {
outline: 0;
}
.MenuPopup {
box-sizing: border-box;
padding-block: 0.25rem;
border-radius: 0.375rem;
background-color: canvas;
color: var(--color-gray-900);
transform-origin: var(--transform-origin);
&[data-ending-style] {
opacity: 0;
transition: opacity 150ms;
}
&[data-instant] {
transition: none;
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow: 0 10px 15px -3px var(--color-gray-200), 0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.MenuItem {
outline: 0;
cursor: default;
user-select: none;
padding: 0.5rem 1rem;
display: flex;
font-size: 0.875rem;
line-height: 1rem;
align-items: center;
justify-content: space-between;
gap: 1rem;
&[data-popup-open] {
z-index: 0;
position: relative;
}
&[data-popup-open]::before {
content: "";
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0.25rem;
border-radius: 0.25rem;
background-color: var(--color-gray-100);
}
&[data-highlighted] {
z-index: 0;
position: relative;
color: var(--color-gray-50);
}
&[data-highlighted]::before {
content: "";
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0.25rem;
border-radius: 0.25rem;
background-color: var(--color-gray-900);
}
}
.MenuSeparator {
margin: 0.375rem 1rem;
height: 1px;
background-color: var(--color-gray-200);
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Menubar } from "@base-ui/react/menubar";
import { Menu } from "@base-ui/react/menu";
import styles from "./index.module.css";
export default function ExampleMenubar() {
return (
<Menubar className={styles.Menubar}>
<Menu.Root>
<Menu.Trigger className={styles.MenuTrigger}>File</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner className={styles.MenuPositioner} sideOffset={6} alignOffset={-2}>
<Menu.Popup className={styles.MenuPopup}>
<Menu.Item className={styles.MenuItem} onClick={handleClick}>
New
</Menu.Item>
<Menu.Item className={styles.MenuItem} onClick={handleClick}>
Open
</Menu.Item>
<Menu.Item className={styles.MenuItem} onClick={handleClick}>
Save
</Menu.Item>
<Menu.SubmenuRoot>
<Menu.SubmenuTrigger className={styles.MenuItem}>
Export
<ChevronRightIcon />
</Menu.SubmenuTrigger>
<Menu.Portal>
<Menu.Positioner alignOffset={-4}>
<Menu.Popup className={styles.MenuPopup}>
<Menu.Item className={styles.MenuItem} onClick={handleClick}>
PDF
</Menu.Item>
<Menu.Item className={styles.MenuItem} onClick={handleClick}>
PNG
</Menu.Item>
<Menu.Item className={styles.MenuItem} onClick={handleClick}>
SVG
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.SubmenuRoot>
<Menu.Separator className={styles.MenuSeparator} />
<Menu.Item className={styles.MenuItem} onClick={handleClick}>
Print
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
<Menu.Root>
<Menu.Trigger className={styles.MenuTrigger}>Edit</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner className={styles.MenuPositioner} sideOffset={6}>
<Menu.Popup className={styles.MenuPopup}>
<Menu.Item className={styles.MenuItem} onClick={handleClick}>
Cut
</Menu.Item>
<Menu.Item className={styles.MenuItem} onClick={handleClick}>
Copy
</Menu.Item>
<Menu.Item className={styles.MenuItem} onClick={handleClick}>
Paste
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
<Menu.Root>
<Menu.Trigger className={styles.MenuTrigger}>View</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner className={styles.MenuPositioner} sideOffset={6}>
<Menu.Popup className={styles.MenuPopup}>
<Menu.Item className={styles.MenuItem} onClick={handleClick}>
Zoom In
</Menu.Item>
<Menu.Item className={styles.MenuItem} onClick={handleClick}>
Zoom Out
</Menu.Item>
<Menu.SubmenuRoot>
<Menu.SubmenuTrigger className={styles.MenuItem}>
Layout
<ChevronRightIcon />
</Menu.SubmenuTrigger>
<Menu.Portal>
<Menu.Positioner alignOffset={-4}>
<Menu.Popup className={styles.MenuPopup}>
<Menu.Item className={styles.MenuItem} onClick={handleClick}>
Single Page
</Menu.Item>
<Menu.Item className={styles.MenuItem} onClick={handleClick}>
Two Pages
</Menu.Item>
<Menu.Item className={styles.MenuItem} onClick={handleClick}>
Continuous
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.SubmenuRoot>
<Menu.Separator className={styles.MenuSeparator} />
<Menu.Item className={styles.MenuItem} onClick={handleClick}>
Full Screen
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
<Menu.Root disabled>
<Menu.Trigger className={styles.MenuTrigger}>Help</Menu.Trigger>
</Menu.Root>
</Menubar>
);
}
function handleClick(event: React.MouseEvent<HTMLElement>) {
// eslint-disable-next-line no-console
console.log(`${event.currentTarget.textContent} clicked`);
}
function ChevronRightIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" {...props}>
<path d="M6 12L10 8L6 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
```
## Anatomy
Import the component and assemble its parts:
```jsx title="Anatomy"
import { Menubar } from "@base-ui/react/menubar";
import { Menu } from "@base-ui/react/menu";
<Menubar>
<Menu.Root>
<Menu.Trigger />
<Menu.Portal>
<Menu.Backdrop />
<Menu.Positioner>
<Menu.Popup>
<Menu.Arrow />
<Menu.Item />
<Menu.Separator />
<Menu.Group>
<Menu.GroupLabel />
</Menu.Group>
<Menu.RadioGroup>
<Menu.RadioItem />
</Menu.RadioGroup>
<Menu.CheckboxItem />
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
</Menubar>;
```
## API reference
The container for menus.
**Menubar Props:**
| Prop | Type | Default | Description |
| :---------- | :--------------------------------------------------------------------------- | :------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| loopFocus | `boolean` | `true` | Whether to loop keyboard focus back to the first item when the end of the list is reached while using the arrow keys. |
| modal | `boolean` | `true` | Whether the menubar is modal. |
| disabled | `boolean` | `false` | Whether the whole menubar is disabled. |
| orientation | `Menu.Root.Orientation` | `'horizontal'` | The orientation of the menubar. |
| className | `string \| ((state: Menubar.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Menubar.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Menubar.State) => ReactElement)` | - | Allows you to replace the component’s HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
references/merge-props.md
---
title: mergeProps
subtitle: A utility to merge multiple sets of React props.
description: A utility to merge multiple sets of React props, handling event handlers, className, and style props intelligently.
---
# mergeProps
A utility to merge multiple sets of React props, handling event handlers, className, and style props intelligently.
`mergeProps` helps you combine multiple prop objects (for example, internal props + user props) into a single set of props you can spread onto an element.
It behaves like `Object.assign` (rightmost wins) with a few special cases, so common React patterns work as expected.
## How merging works
## v1.4.1 notes
- Multi-argument event handler forwarding was fixed in `mergeProps`. If a Base UI component passes extra callback details after the event, merged handlers now receive the full argument list more reliably.
- Keep merged handlers side-effect-safe and avoid assuming the event is the only parameter when wrapping advanced components.
- For most keys (everything except `className`, `style`, and event handlers), the value from the rightmost object wins:
```ts title="returns { id: 'b', dir: 'ltr' }"
mergeProps({ id: "a", dir: "ltr" }, { id: "b" });
```
- `ref` is not merged. Only the rightmost ref is kept:
```ts title="only refB is used"
mergeProps({ ref: refA }, { ref: refB });
```
- `className` values are concatenated right-to-left (rightmost first):
```ts title="className is 'b a'"
mergeProps({ className: "a" }, { className: "b" });
```
- `style` objects are merged, with keys from the rightmost style overwriting earlier ones.
- Event handlers are merged and executed right-to-left (rightmost first):
```ts title="b runs before a"
mergeProps({ onClick: a }, { onClick: b });
```
- For React synthetic events, Base UI adds `event.preventBaseUIHandler()`. Calling it prevents Base UI's internal logic from running.
This does not call `preventDefault()` or `stopPropagation()`.
- For non-synthetic events (custom events with primitive/object values), this mechanism isn't available and all handlers always execute.
### Preventing Base UI's default behavior
Use `event.preventBaseUIHandler()` inside a merged event handler to stop Base UI's internal logic for that event while still allowing your custom handler to run. This is useful when you want to override or block default behavior without calling `preventDefault()` or `stopPropagation()`.
Example:
```tsx
onClick(event) {
event.preventBaseUIHandler();
// Your custom behavior here
}
```
## Demo
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Container {
display: flex;
align-items: center;
gap: 1rem;
}
.ToggleRow {
display: flex;
align-items: center;
gap: 0.75rem;
}
.Label {
font-size: 1rem;
line-height: 1.5rem;
color: var(--color-gray-900);
}
.Panel {
display: flex;
gap: 1px;
border: 1px solid var(--color-gray-200);
background-color: var(--color-gray-50);
border-radius: 0.375rem;
padding: 0.125rem;
}
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
padding: 0;
margin: 0;
outline: 0;
border: 0;
border-radius: 0.25rem;
background-color: transparent;
color: var(--color-gray-600);
user-select: none;
&:focus-visible {
background-color: transparent;
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-200);
}
&[data-pressed] {
color: var(--color-gray-900);
}
}
.Icon {
width: 1.25rem;
height: 1.25rem;
}
.LockButton {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { mergeProps } from "@base-ui/react/merge-props";
import { Toggle } from "@base-ui/react/toggle";
import styles from "./index.module.css";
export default function ExamplePreventBaseUIHandler() {
const [locked, setLocked] = React.useState(true);
const [pressed, setPressed] = React.useState(true);
const getToggleProps = (props: React.ComponentProps<"button">) =>
mergeProps<"button">(props, {
onClick(event) {
if (locked) {
event.preventBaseUIHandler();
}
},
});
return (
<div className={styles.Container}>
<div className={styles.ToggleRow}>
<div className={styles.Panel}>
<Toggle
aria-label="Favorite"
pressed={pressed}
onPressedChange={setPressed}
className={styles.Button}
render={(props, state) => (
<button type="button" {...getToggleProps(props)}>
{state.pressed ? <HeartFilledIcon className={styles.Icon} /> : <HeartOutlineIcon className={styles.Icon} />}
</button>
)}
/>
</div>
<span className={styles.Label}>Favorite {locked ? "(locked)" : "(unlocked)"}</span>
</div>
<button type="button" className={styles.LockButton} onClick={() => setLocked((l) => !l)}>
{locked ? "Unlock" : "Lock"}
</button>
</div>
);
}
function HeartFilledIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentcolor" {...props}>
<path d="M7.99961 13.8667C7.88761 13.8667 7.77561 13.8315 7.68121 13.7611C7.43321 13.5766 1.59961 9.1963 1.59961 5.8667C1.59961 3.80856 3.27481 2.13336 5.33294 2.13336C6.59054 2.13336 7.49934 2.81176 7.99961 3.3131C8.49988 2.81176 9.40868 2.13336 10.6663 2.13336C12.7244 2.13336 14.3996 3.80803 14.3996 5.8667C14.3996 9.1963 8.56601 13.5766 8.31801 13.7616C8.22361 13.8315 8.11161 13.8667 7.99961 13.8667Z" />
</svg>
);
}
function HeartOutlineIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentcolor" {...props}>
<path fillRule="evenodd" clipRule="evenodd" d="M7.99961 4.8232L7.24456 4.06654C6.84123 3.66235 6.18866 3.20003 5.33294 3.20003C3.86391 3.20003 2.66628 4.39767 2.66628 5.8667C2.66628 6.4079 2.91276 7.1023 3.41967 7.91383C3.91548 8.70759 4.59649 9.51244 5.31278 10.2503C6.38267 11.3525 7.47318 12.2465 7.99983 12.6605C8.52734 12.2456 9.61718 11.352 10.6864 10.2504C11.4027 9.51248 12.0837 8.70762 12.5796 7.91384C13.0865 7.1023 13.3329 6.4079 13.3329 5.8667C13.3329 4.39723 12.1354 3.20003 10.6663 3.20003C9.81056 3.20003 9.15799 3.66235 8.75466 4.06654L7.99961 4.8232ZM7.98574 3.29926C7.48264 2.79938 6.57901 2.13336 5.33294 2.13336C3.27481 2.13336 1.59961 3.80856 1.59961 5.8667C1.59961 9.1963 7.43321 13.5766 7.68121 13.7611C7.77561 13.8315 7.88761 13.8667 7.99961 13.8667C8.11161 13.8667 8.22361 13.8315 8.31801 13.7616C8.56601 13.5766 14.3996 9.1963 14.3996 5.8667C14.3996 3.80803 12.7244 2.13336 10.6663 2.13336C9.42013 2.13336 8.51645 2.79947 8.01337 3.29936C8.00877 3.30393 8.00421 3.30849 7.99967 3.31303C7.99965 3.31305 7.99963 3.31307 7.99961 3.3131C7.99502 3.3085 7.9904 3.30389 7.98574 3.29926Z" />
</svg>
);
}
```
When using the function form of the `render` prop, props are not merged automatically.
You can use `mergeProps` to combine Base UI's props with your own, and call `preventBaseUIHandler()` to stop Base UI's internal logic from running:
## Passing a function instead of an object
Each argument can be a props object or a function that receives the merged props up to that point (left to right) and returns a props object.
This is useful when you need to compute the next props from whatever has already been merged.
Note that the function's return value completely replaces the accumulated props up to that point.
If you want to chain event handlers from the previous props, you must call them manually:
```tsx title="Manually chaining handlers in a function"
const merged = mergeProps(
{
onClick(event) {
// Handler from previous props
},
},
(props) => ({
onClick(event) {
// Manually call the previous handler
props.onClick?.(event);
// Your logic here
},
}),
);
```
## API reference
### mergeProps
This function accepts up to 5 arguments, each being either a props object or a function that returns a props object.
If you need to merge more than 5 sets of props, use `mergePropsN` instead.
Merges multiple sets of React props. It follows the Object.assign pattern where the rightmost object's fields overwrite
the conflicting ones from others. This doesn't apply to event handlers, `className` and `style` props. Event handlers are merged and called in right-to-left order (rightmost handler executes first, leftmost last).
For React synthetic events, the rightmost handler can prevent prior (left-positioned) handlers from executing
by calling `event.preventBaseUIHandler()`. For non-synthetic events (custom events with primitive/object values),
all handlers always execute without prevention capability. The `className` prop is merged by concatenating classes in right-to-left order (rightmost class appears first in the string).
The `style` prop is merged with rightmost styles overwriting the prior ones. Props can either be provided as objects or as functions that take the previous props as an argument.
The function will receive the merged props up to that point (going from left to right):
so in the case of `(obj1, obj2, fn, obj3)`, `fn` will receive the merged props of `obj1` and `obj2`.
The function is responsible for chaining event handlers if needed (i.e. we don't run the merge logic). Event handlers returned by the functions are not automatically prevented when `preventBaseUIHandler` is called.
They must check `event.baseUIHandlerPrevented` themselves and bail out if it's true.**`ref` is not merged.**
**Parameters:**
| Parameter | Type | Default | Description |
| :-------- | :------------------------ | :------ | :--------------------------------------------------------------------------------------------- |
| a | `InputProps<ElementType>` | - | Props object to merge. |
| b | `InputProps<ElementType>` | - | Props object to merge. The function will overwrite conflicting props from `a`. |
| c? | `InputProps<ElementType>` | - | Props object to merge. The function will overwrite conflicting props from previous parameters. |
| d? | `InputProps<ElementType>` | - | Props object to merge. The function will overwrite conflicting props from previous parameters. |
| e? | `InputProps<ElementType>` | - | Props object to merge. The function will overwrite conflicting props from previous parameters. |
**Return Value:**
| Type | Description |
| :--- | :---------------- |
| `{}` | The merged props. |
### mergePropsN
This function accepts an array of props objects or functions that return props objects.
It is slightly less efficient than `mergeProps`, so only use it when you need to merge more than 5 sets of props.
Merges an arbitrary number of React props using the same logic as {@link mergeProps}.
This function accepts an array of props instead of individual arguments.
This has slightly lower performance than {@link mergeProps} due to accepting an array
instead of a fixed number of arguments. Prefer {@link mergeProps} when merging 5 or
fewer prop sets for better performance.
**Parameters:**
| Parameter | Type | Default | Description |
| :-------- | :-------------------------- | :------ | :----------------------- |
| props | `InputProps<ElementType>[]` | - | Array of props to merge. |
**Return Value:**
| Type | Description |
| :--- | :---------------- |
| `{}` | The merged props. |
references/meter.md
---
title: Meter
subtitle: A graphical display of a numeric value within a range.
description: A high-quality, unstyled React meter component that provides a graphical display of a numeric value.
---
# Meter
A high-quality, unstyled React meter component that provides a graphical display of a numeric value.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import { Meter } from "@base-ui/react/meter";
export default function ExampleMeter() {
return (
<Meter.Root className="box-border grid w-48 grid-cols-2 gap-y-2" value={24}>
<Meter.Label className="text-sm font-medium text-gray-900">Storage Used</Meter.Label>
<Meter.Value className="col-start-2 m-0 text-right text-sm leading-5 text-gray-900" />
<Meter.Track className="col-span-2 block h-2 w-48 overflow-hidden bg-gray-100 shadow-[inset_0_0_0_1px] shadow-gray-200">
<Meter.Indicator className="block bg-gray-500 transition-all duration-500" />
</Meter.Track>
</Meter.Root>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Meter {
box-sizing: border-box;
display: grid;
grid-template-columns: 1fr 1fr;
grid-row-gap: 0.5rem;
width: 12rem;
}
.Label {
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
color: var(--color-gray-900);
}
.Value {
grid-column-start: 2;
margin: 0;
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--color-gray-900);
text-align: right;
}
.Track {
grid-column: 1 / 3;
overflow: hidden;
background-color: var(--color-gray-100);
box-shadow: inset 0 0 0 1px var(--color-gray-200);
height: 0.5rem;
}
.Indicator {
background-color: var(--color-gray-500);
transition: width 500ms;
}
```
```tsx
/* index.tsx */
import { Meter } from "@base-ui/react/meter";
import styles from "./index.module.css";
export default function ExampleMeter() {
return (
<Meter.Root className={styles.Meter} value={24}>
<Meter.Label className={styles.Label}>Storage Used</Meter.Label>
<Meter.Value className={styles.Value} />
<Meter.Track className={styles.Track}>
<Meter.Indicator className={styles.Indicator} />
</Meter.Track>
</Meter.Root>
);
}
```
## Anatomy
Import the component and assemble its parts:
```jsx title="Anatomy"
import { Meter } from "@base-ui/react/meter";
<Meter.Root>
<Meter.Label />
<Meter.Track>
<Meter.Indicator />
</Meter.Track>
<Meter.Value />
</Meter.Root>;
```
## API reference
### Root
Groups all parts of the meter and provides the value for screen readers.
Renders a `<div>` element.
**Root Props:**
| Prop | Type | Default | Description |
| :--------------- | :------------------------------------------------------------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| value | `number` | - | The current value. |
| aria-valuetext | `string` | - | A string value that provides a user-friendly name for `aria-valuenow`, the current value of the meter. |
| getAriaValueText | `((formattedValue: string, value: number) => string)` | - | A function that returns a string value that provides a human-readable text alternative for `aria-valuenow`, the current value of the meter. |
| locale | `Intl.LocalesArgument` | - | The locale used by `Intl.NumberFormat` when formatting the value.
Defaults to the user's runtime locale. |
| min | `number` | `0` | The minimum value |
| max | `number` | `100` | The maximum value |
| format | `Intl.NumberFormatOptions` | - | Options to format the value. |
| className | `string \| ((state: Meter.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Meter.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Meter.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Track
Contains the meter indicator and represents the entire range of the meter.
Renders a `<div>` element.
**Track Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Meter.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Meter.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Meter.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Indicator
Visualizes the position of the value along the range.
Renders a `<div>` element.
**Indicator Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Meter.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Meter.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Meter.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Value
A text element displaying the current value.
Renders a `<span>` element.
**Value Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| children | `((formattedValue: string, value: number) => ReactNode) \| null` | - | - |
| className | `string \| ((state: Meter.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Meter.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Meter.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Label
An accessible label for the meter.
Renders a `<span>` element.
**Label Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Meter.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Meter.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Meter.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
references/navigation-menu.md
---
title: Navigation Menu
subtitle: A collection of links and menus for website navigation.
description: A high-quality, unstyled React navigation menu component that displays a collection of links and menus for website navigation.
---
# Navigation Menu
A high-quality, unstyled React navigation menu component that displays a collection of links and menus for website navigation.
## v1.4.0 notes
- `NavigationMenu.Root` gained generic `Value` typing for stronger controlled-state typing.
- Pointer-event blocking, reverse `Shift+Tab` submenu re-entry, nested inline hover handoff, and reopen width transitions were fixed.
- Remove any local workarounds for invalid `aria-orientation`; the component no longer emits that attribute incorrectly.
## v1.5.0 notes
- RTL behavior is corrected again in the `1.5.0` line. Keep `DirectionProvider` plus DOM `dir="rtl"`, but retest submenu motion and positioning before preserving old RTL patches.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import * as React from "react";
import { NavigationMenu } from "@base-ui/react/navigation-menu";
export default function ExampleNavigationMenu() {
return (
<NavigationMenu.Root className="min-w-max rounded-lg bg-gray-50 p-1 text-gray-900">
<NavigationMenu.List className="relative flex">
<NavigationMenu.Item>
<NavigationMenu.Trigger className={triggerClassName}>
Overview
<NavigationMenu.Icon className="transition-transform duration-200 ease-in-out data-[popup-open]:rotate-180">
<ChevronDownIcon />
</NavigationMenu.Icon>
</NavigationMenu.Trigger>
<NavigationMenu.Content className={contentClassName}>
<ul className="grid list-none grid-cols-1 gap-0 xs:grid-cols-[12rem_12rem]">
{overviewLinks.map((item) => (
<li key={item.href}>
<Link href={item.href} className={linkCardClassName}>
<h3 className="m-0 mb-1 text-base leading-5 font-medium">{item.title}</h3>
<p className="m-0 text-sm leading-5 text-gray-500">{item.description}</p>
</Link>
</li>
))}
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Trigger className={triggerClassName}>
Handbook
<NavigationMenu.Icon className="transition-transform duration-200 ease-in-out data-[popup-open]:rotate-180">
<ChevronDownIcon />
</NavigationMenu.Icon>
</NavigationMenu.Trigger>
<NavigationMenu.Content className={contentClassName}>
<ul className="flex max-w-[400px] flex-col justify-center">
{handbookLinks.map((item) => (
<li key={item.href}>
<Link href={item.href} className={linkCardClassName}>
<h3 className="m-0 mb-1 text-base leading-5 font-medium">{item.title}</h3>
<p className="m-0 text-sm leading-5 text-gray-500">{item.description}</p>
</Link>
</li>
))}
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<Link className={triggerClassName} href="https://github.com/mui/base-ui">
GitHub
</Link>
</NavigationMenu.Item>
</NavigationMenu.List>
<NavigationMenu.Portal>
<NavigationMenu.Positioner
sideOffset={10}
collisionPadding={{ top: 5, bottom: 5, left: 20, right: 20 }}
collisionAvoidance={{ side: "none" }}
className="box-border h-[var(--positioner-height)] w-[var(--positioner-width)] max-w-[var(--available-width)] transition-[top,left,right,bottom] duration-[var(--duration)] ease-[var(--easing)] before:absolute before:content-[''] data-[instant]:transition-none data-[side=bottom]:before:top-[-10px] data-[side=bottom]:before:right-0 data-[side=bottom]:before:left-0 data-[side=bottom]:before:h-2.5 data-[side=left]:before:top-0 data-[side=left]:before:right-[-10px] data-[side=left]:before:bottom-0 data-[side=left]:before:w-2.5 data-[side=right]:before:top-0 data-[side=right]:before:bottom-0 data-[side=right]:before:left-[-10px] data-[side=right]:before:w-2.5 data-[side=top]:before:right-0 data-[side=top]:before:bottom-[-10px] data-[side=top]:before:left-0 data-[side=top]:before:h-2.5"
style={{
["--duration" as string]: "0.35s",
["--easing" as string]: "cubic-bezier(0.22, 1, 0.36, 1)",
}}
>
<NavigationMenu.Popup className="data-[ending-style]:easing-[ease] relative h-[var(--popup-height)] origin-[var(--transform-origin)] rounded-lg bg-[canvas] text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 transition-[opacity,transform,width,height,scale,translate] duration-[var(--duration)] ease-[var(--easing)] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[ending-style]:duration-150 data-[starting-style]:scale-90 data-[starting-style]:opacity-0 w-[var(--popup-width)] xs:w-[var(--popup-width)] dark:shadow-none dark:-outline-offset-1 dark:outline-gray-300">
<NavigationMenu.Arrow className="flex transition-[left] duration-[var(--duration)] ease-[var(--easing)] data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180">
<ArrowSvg />
</NavigationMenu.Arrow>
<NavigationMenu.Viewport className="relative h-full w-full overflow-hidden" />
</NavigationMenu.Popup>
</NavigationMenu.Positioner>
</NavigationMenu.Portal>
</NavigationMenu.Root>
);
}
function Link(props: NavigationMenu.Link.Props) {
return (
<NavigationMenu.Link
render={
// Use the `render` prop to render your framework's Link component
// for client-side routing.
// e.g. `<NextLink href={props.href} />` instead of `<a />`.
<a />
}
{...props}
/>
);
}
function ChevronDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...props}>
<path d="M1 3.5L5 7.5L9 3.5" stroke="currentColor" strokeWidth="1.5" />
</svg>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className="fill-[canvas]" />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className="fill-gray-200 dark:fill-none" />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className="dark:fill-gray-300" />
</svg>
);
}
const triggerClassName = "box-border flex items-center justify-center gap-1.5 h-10 " + "px-2 xs:px-3.5 m-0 rounded-md bg-gray-50 text-gray-900 font-medium " + "text-[0.925rem] xs:text-base leading-6 select-none no-underline " + "hover:bg-gray-100 active:bg-gray-100 data-[popup-open]:bg-gray-100 " + "focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 focus-visible:relative";
const contentClassName = "w-[calc(100vw_-_40px)] h-full p-6 xs:w-max xs:min-w-[400px] xs:w-max " + "transition-[opacity,transform,translate] duration-[var(--duration)] ease-[var(--easing)] " + "data-[starting-style]:opacity-0 data-[ending-style]:opacity-0 " + "data-[starting-style]:data-[activation-direction=left]:translate-x-[-50%] " + "data-[starting-style]:data-[activation-direction=right]:translate-x-[50%] " + "data-[ending-style]:data-[activation-direction=left]:translate-x-[50%] " + "data-[ending-style]:data-[activation-direction=right]:translate-x-[-50%]";
const linkCardClassName = "block rounded-md p-2 xs:p-3 no-underline text-inherit " + "hover:bg-gray-100 focus-visible:relative focus-visible:outline focus-visible:outline-2 " + "focus-visible:-outline-offset-1 focus-visible:outline-blue-800";
const overviewLinks = [
{
href: "/react/overview/quick-start",
title: "Quick Start",
description: "Install and assemble your first component.",
},
{
href: "/react/overview/accessibility",
title: "Accessibility",
description: "Learn how we build accessible components.",
},
{
href: "/react/overview/releases",
title: "Releases",
description: "See what’s new in the latest Base UI versions.",
},
{
href: "/react/overview/about",
title: "About",
description: "Learn more about Base UI and our mission.",
},
] as const;
const handbookLinks = [
{
href: "/react/handbook/styling",
title: "Styling",
description: "Base UI components can be styled with plain CSS, Tailwind CSS, CSS-in-JS, or CSS Modules.",
},
{
href: "/react/handbook/animation",
title: "Animation",
description: "Base UI components can be animated with CSS transitions, CSS animations, or JavaScript libraries.",
},
{
href: "/react/handbook/composition",
title: "Composition",
description: "Base UI components can be replaced and composed with your own existing components.",
},
] as const;
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Root {
background-color: var(--color-gray-50);
border-radius: 0.5rem;
padding: 0.25rem;
color: var(--color-gray-900);
min-width: max-content;
}
.List {
display: flex;
position: relative;
list-style: none;
padding: 0;
margin: 0;
}
.Trigger {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: none;
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
text-decoration: none;
@media (max-width: 500px) {
font-size: 0.925rem;
padding: 0 0.5rem;
}
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
position: relative;
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Icon {
transition: transform 0.2s ease;
&[data-popup-open] {
transform: rotate(180deg);
}
}
.Positioner {
--easing: cubic-bezier(0.22, 1, 0.36, 1);
--duration: 0.35s;
box-sizing: border-box;
transition-property: top, left, right, bottom;
transition-duration: var(--duration);
transition-timing-function: var(--easing);
width: var(--positioner-width);
height: var(--positioner-height);
max-width: var(--available-width);
&::before {
content: "";
position: absolute;
}
&[data-side="top"]::before {
left: 0;
right: 0;
bottom: -10px;
height: 10px;
}
&[data-side="bottom"]::before {
left: 0;
right: 0;
top: -10px;
height: 10px;
}
&[data-side="left"]::before {
top: 0;
bottom: 0;
right: -10px;
width: 10px;
}
&[data-side="right"]::before {
top: 0;
bottom: 0;
left: -10px;
width: 10px;
}
&[data-instant] {
transition: none;
}
}
.Popup {
position: relative;
box-sizing: border-box;
border-radius: 0.5rem;
background-color: canvas;
color: var(--color-gray-900);
transform-origin: var(--transform-origin);
transition-property: opacity, transform, width, height;
transition-duration: var(--duration);
transition-timing-function: var(--easing);
width: var(--popup-width);
height: var(--popup-height);
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
&[data-ending-style] {
transition-timing-function: ease;
transition-duration: 0.15s;
}
}
@media (prefers-color-scheme: light) {
.Popup {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
}
@media (prefers-color-scheme: dark) {
.Popup {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Content {
box-sizing: border-box;
transition:
opacity calc(var(--duration) * 0.5) ease,
transform var(--duration) var(--easing);
padding: 1.5rem;
width: calc(100vw - 40px);
height: 100%;
@media (min-width: 500px) {
width: max-content;
min-width: 400px;
}
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
}
&[data-starting-style] {
&[data-activation-direction="left"] {
transform: translateX(-50%);
}
&[data-activation-direction="right"] {
transform: translateX(50%);
}
}
&[data-ending-style] {
&[data-activation-direction="left"] {
transform: translateX(50%);
}
&[data-activation-direction="right"] {
transform: translateX(-50%);
}
}
}
.Viewport {
position: relative;
overflow: hidden;
width: 100%;
height: 100%;
}
.GridLinkList {
display: grid;
grid-template-columns: 12rem 12rem;
list-style: none;
padding: 0;
margin: 0;
@media (max-width: 500px) {
grid-template-columns: 1fr;
}
}
.FlexLinkList {
display: flex;
flex-direction: column;
justify-content: center;
max-width: 400px;
padding: 0;
margin: 0;
list-style: none;
}
.LinkCard {
box-sizing: border-box;
display: block;
padding: 0.5rem;
border-radius: 0.375rem;
text-decoration: none;
color: inherit;
border: none;
background-color: transparent;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:focus-visible {
position: relative;
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
@media (min-width: 425px) {
padding: 0.75rem;
}
}
.LinkTitle {
margin: 0 0 4px;
font-size: 1rem;
font-weight: 500;
line-height: 1.25rem;
}
.LinkDescription {
margin: 0;
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--color-gray-500);
}
.Arrow {
display: flex;
transition: left calc(var(--duration)) var(--easing);
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
```
```tsx
/* index.tsx */
import * as React from "react";
import { NavigationMenu } from "@base-ui/react/navigation-menu";
import styles from "./index.module.css";
export default function ExampleNavigationMenu() {
return (
<NavigationMenu.Root className={styles.Root}>
<NavigationMenu.List className={styles.List}>
<NavigationMenu.Item>
<NavigationMenu.Trigger className={styles.Trigger}>
Overview
<NavigationMenu.Icon className={styles.Icon}>
<ChevronDownIcon />
</NavigationMenu.Icon>
</NavigationMenu.Trigger>
<NavigationMenu.Content className={styles.Content}>
<ul className={styles.GridLinkList}>
{overviewLinks.map((item) => (
<li key={item.href}>
<Link className={styles.LinkCard} href={item.href}>
<h3 className={styles.LinkTitle}>{item.title}</h3>
<p className={styles.LinkDescription}>{item.description}</p>
</Link>
</li>
))}
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Trigger className={styles.Trigger}>
Handbook
<NavigationMenu.Icon className={styles.Icon}>
<ChevronDownIcon />
</NavigationMenu.Icon>
</NavigationMenu.Trigger>
<NavigationMenu.Content className={styles.Content}>
<ul className={styles.FlexLinkList}>
{handbookLinks.map((item) => (
<li key={item.href}>
<Link className={styles.LinkCard} href={item.href}>
<h3 className={styles.LinkTitle}>{item.title}</h3>
<p className={styles.LinkDescription}>{item.description}</p>
</Link>
</li>
))}
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<Link className={styles.Trigger} href="https://github.com/mui/base-ui">
GitHub
</Link>
</NavigationMenu.Item>
</NavigationMenu.List>
<NavigationMenu.Portal>
<NavigationMenu.Positioner className={styles.Positioner} sideOffset={10} collisionPadding={{ top: 5, bottom: 5, left: 20, right: 20 }} collisionAvoidance={{ side: "none" }}>
<NavigationMenu.Popup className={styles.Popup}>
<NavigationMenu.Arrow className={styles.Arrow}>
<ArrowSvg />
</NavigationMenu.Arrow>
<NavigationMenu.Viewport className={styles.Viewport} />
</NavigationMenu.Popup>
</NavigationMenu.Positioner>
</NavigationMenu.Portal>
</NavigationMenu.Root>
);
}
function Link(props: NavigationMenu.Link.Props) {
return (
<NavigationMenu.Link
render={
// Use the `render` prop to render your framework's Link component
// for client-side routing.
// e.g. `<NextLink href={props.href} />` instead of `<a />`.
<a />
}
{...props}
/>
);
}
function ChevronDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...props}>
<path d="M1 3.5L5 7.5L9 3.5" stroke="currentcolor" strokeWidth="1.5" />
</svg>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className={styles.ArrowFill} />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className={styles.ArrowOuterStroke} />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className={styles.ArrowInnerStroke} />
</svg>
);
}
const overviewLinks = [
{
href: "/react/overview/quick-start",
title: "Quick Start",
description: "Install and assemble your first component.",
},
{
href: "/react/overview/accessibility",
title: "Accessibility",
description: "Learn how we build accessible components.",
},
{
href: "/react/overview/releases",
title: "Releases",
description: "See what’s new in the latest Base UI versions.",
},
{
href: "/react/overview/about",
title: "About",
description: "Learn more about Base UI and our mission.",
},
] as const;
const handbookLinks = [
{
href: "/react/handbook/styling",
title: "Styling",
description: "Base UI components can be styled with plain CSS, Tailwind CSS, CSS-in-JS, or CSS Modules.",
},
{
href: "/react/handbook/animation",
title: "Animation",
description: "Base UI components can be animated with CSS transitions, CSS animations, or JavaScript libraries.",
},
{
href: "/react/handbook/composition",
title: "Composition",
description: "Base UI components can be replaced and composed with your own existing components.",
},
] as const;
```
## Anatomy
Import the component and assemble its parts:
```jsx title="Anatomy"
import { NavigationMenu } from "@base-ui/react/navigation-menu";
<NavigationMenu.Root>
<NavigationMenu.List>
<NavigationMenu.Item>
<NavigationMenu.Trigger>
<NavigationMenu.Icon />
</NavigationMenu.Trigger>
<NavigationMenu.Content>
<NavigationMenu.Link />
</NavigationMenu.Content>
</NavigationMenu.Item>
</NavigationMenu.List>
<NavigationMenu.Portal>
<NavigationMenu.Backdrop />
<NavigationMenu.Positioner>
<NavigationMenu.Popup>
<NavigationMenu.Arrow />
<NavigationMenu.Viewport />
</NavigationMenu.Popup>
</NavigationMenu.Positioner>
</NavigationMenu.Portal>
</NavigationMenu.Root>;
```
## Examples
### Nested submenus
`<NavigationMenu.Root>` component can be nested within a higher-level `<NavigationMenu.Content>` part to create a multi-level navigation menu.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import * as React from "react";
import { NavigationMenu } from "@base-ui/react/navigation-menu";
export default function ExampleNavigationMenu() {
return (
<NavigationMenu.Root className="min-w-max rounded-lg bg-gray-50 p-1 text-gray-900">
<NavigationMenu.List className="relative flex">
<NavigationMenu.Item>
<NavigationMenu.Trigger className={triggerClassName}>
Overview
<NavigationMenu.Icon className="transition-transform duration-200 ease-in-out data-[popup-open]:rotate-180">
<ChevronDownIcon />
</NavigationMenu.Icon>
</NavigationMenu.Trigger>
<NavigationMenu.Content className={contentClassName}>
<ul className="grid list-none grid-cols-1 gap-0 sm:grid-cols-[12rem_12rem]">
{overviewLinks.map((item) => (
<li key={item.href}>
<Link href={item.href} className={linkCardClassName}>
<h3 className="m-0 mb-1 text-base leading-5 font-medium">{item.title}</h3>
<p className="m-0 text-sm leading-5 text-gray-500">{item.description}</p>
</Link>
</li>
))}
<li>
<NavigationMenu.Root orientation="vertical">
<NavigationMenu.Item>
<NavigationMenu.Trigger className={linkCardClassName}>
<span className="m-0 mb-1 text-base leading-5 font-medium">Handbook</span>
<p className="m-0 text-sm leading-5 text-gray-500">How to use Base UI effectively.</p>
<NavigationMenu.Icon className="absolute top-1/2 right-2.5 flex h-2.5 w-2.5 -translate-y-1/2 items-center justify-center transition-transform duration-200 ease-in-out data-[popup-open]:rotate-180">
<ChevronRightIcon />
</NavigationMenu.Icon>
</NavigationMenu.Trigger>
<NavigationMenu.Content className={contentClassName}>
<ul className="flex max-w-[400px] flex-col justify-center">
{handbookLinks.map((item) => (
<li key={item.href}>
<Link href={item.href} className={linkCardClassName}>
<h3 className="m-0 mb-1 text-base leading-5 font-medium">{item.title}</h3>
<p className="m-0 text-sm leading-5 text-gray-500">{item.description}</p>
</Link>
</li>
))}
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Portal>
<NavigationMenu.Positioner
sideOffset={24}
alignOffset={-24}
align="end"
side="right"
className="box-border h-[var(--positioner-height)] w-[var(--positioner-width)] max-w-[var(--available-width)] transition-[top,left,right,bottom] duration-[var(--duration)] ease-[var(--easing)] before:absolute before:content-[''] data-[instant]:transition-none data-[side=bottom]:before:top-[-10px] data-[side=bottom]:before:right-0 data-[side=bottom]:before:left-0 data-[side=bottom]:before:h-2.5 data-[side=left]:before:top-0 data-[side=left]:before:right-[-10px] data-[side=left]:before:bottom-0 data-[side=left]:before:w-2.5 data-[side=right]:before:top-0 data-[side=right]:before:bottom-0 data-[side=right]:before:left-[-10px] data-[side=right]:before:w-2.5 data-[side=top]:before:right-0 data-[side=top]:before:bottom-[-10px] data-[side=top]:before:left-0 data-[side=top]:before:h-2.5"
style={{
["--duration" as string]: "0.35s",
["--easing" as string]: "cubic-bezier(0.22, 1, 0.36, 1)",
}}
>
<NavigationMenu.Popup className="data-[ending-style]:easing-[ease] relative h-[var(--popup-height)] w-[300px] origin-[var(--transform-origin)] rounded-lg bg-[canvas] text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 transition-[opacity,transform,width,height,scale,translate] duration-[var(--duration)] ease-[var(--easing)] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[ending-style]:duration-150 data-[starting-style]:scale-90 data-[starting-style]:opacity-0 min-[500px]:w-[var(--popup-width)] dark:shadow-none dark:-outline-offset-1 dark:outline-gray-300">
<NavigationMenu.Viewport className="relative h-full w-full overflow-hidden" />
</NavigationMenu.Popup>
</NavigationMenu.Positioner>
</NavigationMenu.Portal>
</NavigationMenu.Root>
</li>
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
</NavigationMenu.List>
<NavigationMenu.Portal>
<NavigationMenu.Positioner
sideOffset={10}
collisionPadding={{ top: 5, bottom: 5, left: 20, right: 20 }}
className="box-border h-[var(--positioner-height)] w-[var(--positioner-width)] max-w-[var(--available-width)] transition-[top,left,right,bottom] duration-[var(--duration)] ease-[var(--easing)] before:absolute before:content-[''] data-[instant]:transition-none data-[side=bottom]:before:top-[-10px] data-[side=bottom]:before:right-0 data-[side=bottom]:before:left-0 data-[side=bottom]:before:h-2.5 data-[side=left]:before:top-0 data-[side=left]:before:right-[-10px] data-[side=left]:before:bottom-0 data-[side=left]:before:w-2.5 data-[side=right]:before:top-0 data-[side=right]:before:bottom-0 data-[side=right]:before:left-[-10px] data-[side=right]:before:w-2.5 data-[side=top]:before:right-0 data-[side=top]:before:bottom-[-10px] data-[side=top]:before:left-0 data-[side=top]:before:h-2.5"
style={{
["--duration" as string]: "0.35s",
["--easing" as string]: "cubic-bezier(0.22, 1, 0.36, 1)",
}}
>
<NavigationMenu.Popup className="data-[ending-style]:easing-[ease] relative h-[var(--popup-height)] origin-[var(--transform-origin)] rounded-lg bg-[canvas] text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 transition-[opacity,transform,width,height,scale,translate] duration-[var(--duration)] ease-[var(--easing)] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[ending-style]:duration-150 data-[starting-style]:scale-90 data-[starting-style]:opacity-0 w-[var(--popup-width)] dark:shadow-none dark:-outline-offset-1 dark:outline-gray-300">
<NavigationMenu.Arrow className="flex transition-[left] duration-[var(--duration)] ease-[var(--easing)] data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180">
<ArrowSvg />
</NavigationMenu.Arrow>
<NavigationMenu.Viewport className="relative h-full w-full overflow-hidden" />
</NavigationMenu.Popup>
</NavigationMenu.Positioner>
</NavigationMenu.Portal>
</NavigationMenu.Root>
);
}
function Link(props: NavigationMenu.Link.Props) {
return (
<NavigationMenu.Link
render={
// Use the `render` prop to render your framework's Link component
// for client-side routing.
// e.g. `<NextLink href={props.href} />` instead of `<a />`.
<a />
}
{...props}
/>
);
}
function ChevronDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...props}>
<path d="M1 3.5L5 7.5L9 3.5" stroke="currentColor" strokeWidth="1.5" />
</svg>
);
}
function ChevronRightIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...props}>
<path d="M3.5 1L7.5 5L3.5 9" stroke="currentColor" strokeWidth="1.5" />
</svg>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className="fill-[canvas]" />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className="fill-gray-200 dark:fill-none" />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className="dark:fill-gray-300" />
</svg>
);
}
const triggerClassName = "box-border flex items-center justify-center gap-1.5 h-10 " + "px-2 sm:px-3.5 m-0 rounded-md bg-gray-50 text-gray-900 font-medium " + "text-[0.925rem] sm:text-base leading-6 select-none no-underline " + "hover:bg-gray-100 active:bg-gray-100 data-[popup-open]:bg-gray-100 " + "focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 focus-visible:relative";
const contentClassName = "w-[calc(100vw_-_40px)] h-full p-6 xs:w-max xs:min-w-[400px] xs:w-max " + "transition-[opacity,transform,translate] duration-[var(--duration)] ease-[var(--easing)] " + "data-[starting-style]:opacity-0 data-[ending-style]:opacity-0 " + "data-[starting-style]:data-[activation-direction=left]:translate-x-[-50%] " + "data-[starting-style]:data-[activation-direction=right]:translate-x-[50%] " + "data-[ending-style]:data-[activation-direction=left]:translate-x-[50%] " + "data-[ending-style]:data-[activation-direction=right]:translate-x-[-50%]";
const linkCardClassName = "w-full text-left relative block rounded-md p-2 sm:p-3 no-underline text-inherit " + "hover:bg-gray-100 focus-visible:relative focus-visible:outline focus-visible:outline-2 " + "focus-visible:-outline-offset-1 focus-visible:outline-blue-800 " + "data-[popup-open]:bg-gray-100";
const overviewLinks = [
{
href: "/react/overview/quick-start",
title: "Quick Start",
description: "Install and assemble your first component.",
},
{
href: "/react/overview/accessibility",
title: "Accessibility",
description: "Learn how we build accessible components.",
},
{
href: "/react/overview/releases",
title: "Releases",
description: "See what’s new in the latest Base UI versions.",
},
] as const;
const handbookLinks = [
{
href: "/react/handbook/styling",
title: "Styling",
description: "Base UI components can be styled with plain CSS, Tailwind CSS, CSS-in-JS, or CSS Modules.",
},
{
href: "/react/handbook/animation",
title: "Animation",
description: "Base UI components can be animated with CSS transitions, CSS animations, or JavaScript libraries.",
},
{
href: "/react/handbook/composition",
title: "Composition",
description: "Base UI components can be replaced and composed with your own existing components.",
},
] as const;
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Root {
background-color: var(--color-gray-50);
border-radius: 0.5rem;
padding: 0.25rem;
color: var(--color-gray-900);
min-width: max-content;
}
.List {
display: flex;
position: relative;
list-style: none;
padding: 0;
margin: 0;
}
.Trigger {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: none;
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
text-decoration: none;
@media (max-width: 640px) {
font-size: 0.925rem;
padding: 0 0.5rem;
}
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
position: relative;
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Icon {
transition: transform 0.2s ease;
&[data-popup-open] {
transform: rotate(180deg);
}
}
.Positioner {
--easing: cubic-bezier(0.22, 1, 0.36, 1);
--duration: 0.35s;
box-sizing: border-box;
transition-property: top, left, right, bottom;
transition-duration: var(--duration);
transition-timing-function: var(--easing);
width: var(--positioner-width);
height: var(--positioner-height);
max-width: var(--available-width);
&::before {
content: "";
position: absolute;
}
&[data-side="top"]::before {
left: 0;
right: 0;
bottom: -10px;
height: 10px;
}
&[data-side="bottom"]::before {
left: 0;
right: 0;
top: -10px;
height: 10px;
}
&[data-side="left"]::before {
top: 0;
bottom: 0;
right: -10px;
width: 10px;
}
&[data-side="right"]::before {
top: 0;
bottom: 0;
left: -10px;
width: 10px;
}
&[data-instant] {
transition: none;
}
}
.Popup {
position: relative;
overflow: visible;
box-sizing: border-box;
border-radius: 0.5rem;
background-color: canvas;
color: var(--color-gray-900);
transform-origin: var(--transform-origin);
transition-property: opacity, transform, width, height;
transition-duration: var(--duration);
transition-timing-function: var(--easing);
width: var(--popup-width);
height: var(--popup-height);
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
&[data-ending-style] {
transition-timing-function: ease;
transition-duration: 0.15s;
}
}
@media (prefers-color-scheme: light) {
.Popup {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
}
@media (prefers-color-scheme: dark) {
.Popup {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Content {
box-sizing: border-box;
transition:
opacity calc(var(--duration) * 0.5) ease,
transform var(--duration) var(--easing);
padding: 1.5rem;
width: calc(100vw - 40px);
height: 100%;
@media (min-width: 500px) {
width: max-content;
min-width: 400px;
}
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
}
&[data-starting-style] {
&[data-activation-direction="left"] {
transform: translateX(-50%);
}
&[data-activation-direction="right"] {
transform: translateX(50%);
}
}
&[data-ending-style] {
&[data-activation-direction="left"] {
transform: translateX(50%);
}
&[data-activation-direction="right"] {
transform: translateX(-50%);
}
}
}
.Viewport {
position: relative;
overflow: hidden;
width: 100%;
height: 100%;
}
.GridLinkList {
display: grid;
grid-template-columns: 1fr;
padding: 0;
margin: 0;
list-style: none;
@media (min-width: 640px) {
grid-template-columns: 12rem 12rem;
}
}
.FlexLinkList {
display: flex;
flex-direction: column;
justify-content: center;
max-width: 400px;
padding: 0;
margin: 0;
list-style: none;
}
.LinkCard {
box-sizing: border-box;
position: relative;
display: block;
width: 100%;
height: 100%;
padding: 0.5rem;
border-radius: 0.375rem;
text-decoration: none;
color: inherit;
text-align: left;
border: none;
background-color: transparent;
&[data-popup-open] {
background-color: var(--color-gray-50);
}
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:focus-visible {
position: relative;
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
@media (min-width: 500px) {
padding: 0.75rem;
}
}
.LinkTitle {
margin: 0 0 4px;
font-size: 1rem;
font-weight: 500;
line-height: 1.25rem;
}
.LinkDescription {
margin: 0;
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--color-gray-500);
}
.Arrow {
display: flex;
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.NestedIcon {
position: absolute;
top: 50%;
right: 0.6rem;
display: flex;
align-items: center;
justify-content: center;
width: 0.6rem;
height: 0.6rem;
margin-top: -0.3rem;
transition: transform 0.2s ease;
&[data-popup-open] {
transform: rotate(180deg);
}
}
```
```tsx
/* index.tsx */
import * as React from "react";
import { NavigationMenu } from "@base-ui/react/navigation-menu";
import styles from "./index.module.css";
export default function ExampleNavigationMenu() {
return (
<NavigationMenu.Root className={styles.Root}>
<NavigationMenu.List className={styles.List}>
<NavigationMenu.Item>
<NavigationMenu.Trigger className={styles.Trigger}>
Overview
<NavigationMenu.Icon className={styles.Icon}>
<ChevronDownIcon />
</NavigationMenu.Icon>
</NavigationMenu.Trigger>
<NavigationMenu.Content className={styles.Content}>
<ul className={styles.GridLinkList}>
{overviewLinks.map((item) => (
<li key={item.href}>
<Link className={styles.LinkCard} href={item.href}>
<h3 className={styles.LinkTitle}>{item.title}</h3>
<p className={styles.LinkDescription}>{item.description}</p>
</Link>
</li>
))}
<li>
<NavigationMenu.Root orientation="vertical">
<NavigationMenu.List>
<NavigationMenu.Item>
<NavigationMenu.Trigger className={styles.LinkCard}>
<span className={styles.LinkTitle}>Handbook</span>
<p className={styles.LinkDescription}>How to use Base UI effectively.</p>
<NavigationMenu.Icon className={styles.NestedIcon}>
<ChevronRightIcon />
</NavigationMenu.Icon>
</NavigationMenu.Trigger>
<NavigationMenu.Content className={styles.Content}>
<ul className={styles.FlexLinkList}>
{handbookLinks.map((item) => (
<li key={item.href}>
<Link className={styles.LinkCard} href={item.href}>
<h3 className={styles.LinkTitle}>{item.title}</h3>
<p className={styles.LinkDescription}>{item.description}</p>
</Link>
</li>
))}
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
</NavigationMenu.List>
<NavigationMenu.Portal>
<NavigationMenu.Positioner className={styles.Positioner} sideOffset={24} alignOffset={-24} align="end" side="right">
<NavigationMenu.Popup className={styles.Popup}>
<NavigationMenu.Viewport className={styles.Viewport} />
</NavigationMenu.Popup>
</NavigationMenu.Positioner>
</NavigationMenu.Portal>
</NavigationMenu.Root>
</li>
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
</NavigationMenu.List>
<NavigationMenu.Portal>
<NavigationMenu.Positioner className={styles.Positioner} sideOffset={10} collisionPadding={{ top: 5, bottom: 5, left: 20, right: 20 }}>
<NavigationMenu.Popup className={styles.Popup}>
<NavigationMenu.Arrow className={styles.Arrow}>
<ArrowSvg />
</NavigationMenu.Arrow>
<NavigationMenu.Viewport className={styles.Viewport} />
</NavigationMenu.Popup>
</NavigationMenu.Positioner>
</NavigationMenu.Portal>
</NavigationMenu.Root>
);
}
function Link(props: NavigationMenu.Link.Props) {
return (
<NavigationMenu.Link
render={
// Use the `render` prop to render your framework's Link component
// for client-side routing.
// e.g. `<NextLink href={props.href} />` instead of `<a />`.
<a />
}
{...props}
/>
);
}
function ChevronDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...props}>
<path d="M1 3.5L5 7.5L9 3.5" stroke="currentcolor" strokeWidth="1.5" />
</svg>
);
}
function ChevronRightIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...props}>
<path d="M3.5 1L7.5 5L3.5 9" stroke="currentcolor" strokeWidth="1.5" />
</svg>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className={styles.ArrowFill} />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className={styles.ArrowOuterStroke} />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className={styles.ArrowInnerStroke} />
</svg>
);
}
const overviewLinks = [
{
href: "/react/overview/quick-start",
title: "Quick Start",
description: "Install and assemble your first component.",
},
{
href: "/react/overview/accessibility",
title: "Accessibility",
description: "Learn how we build accessible components.",
},
{
href: "/react/overview/releases",
title: "Releases",
description: "See what’s new in the latest Base UI versions.",
},
] as const;
const handbookLinks = [
{
href: "/react/handbook/styling",
title: "Styling",
description: "Base UI components can be styled with plain CSS, Tailwind CSS, CSS-in-JS, or CSS Modules.",
},
{
href: "/react/handbook/animation",
title: "Animation",
description: "Base UI components can be animated with CSS transitions, CSS animations, or JavaScript libraries.",
},
{
href: "/react/handbook/composition",
title: "Composition",
description: "Base UI components can be replaced and composed with your own existing components.",
},
] as const;
```
### Custom links
The `<NavigationMenu.Link>` part can be customized to render the link from your framework using the `render` prop to enable client-side routing.
```jsx title="Next.js example" {1,7}
import NextLink from 'next/link';
import { NavigationMenu } from '@base-ui/react/navigation-menu';
function Link(props: NavigationMenu.Link.Props) {
return (
<NavigationMenu.Link
render={<NextLink href={props.href} />}
{...props}
/>
);
}
```
### Large menus
When you have large menu content that doesn't fit in the viewport in some cases, you usually have two choices:
1. Compress the navigation menu content
You can change the layout of the navigation menu to render less content or be more compact by reducing the space it takes up.
If your content is flexible, you can use the `max-height` property on `.Popup` to limit the height of the navigation menu to let it compress itself while preventing overflow.
```css title="Compact layout"
.Content,
.Popup {
max-height: var(--available-height);
}
```
2. Make the navigation menu scrollable
```css title="Scrollable layout"
.Content,
.Popup {
max-height: var(--available-height);
}
.Content {
overflow-y: auto;
}
```
Native scrollbars are visible while transitioning content, so we recommend using the [Scroll Area](/react/components/scroll-area.md) component instead of native scrollbars to keep them hidden, which also allows the `Arrow` to be centered correctly.
## API reference
### Root
Groups all parts of the navigation menu.
Renders a `<nav>` element at the root, or `<div>` element when nested.
**Root Props:**
| Prop | Type | Default | Description |
| :------------------- | :--------------------------------------------------------------------------------------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| defaultValue | `any` | `null` | The uncontrolled value of the item that should be initially selected.To render a controlled navigation menu, use the `value` prop instead. |
| value | `any` | `null` | The controlled value of the navigation menu item that should be currently open.
When non-nullish, the menu will be open. When nullish, the menu will be closed.To render an uncontrolled navigation menu, use the `defaultValue` prop instead. |
| onValueChange | `((value: any, eventDetails: NavigationMenu.Root.ChangeEventDetails) => void)` | - | Callback fired when the value changes. |
| actionsRef | `RefObject<NavigationMenu.Root.Actions \| null>` | - | A ref to imperative actions. |
| onOpenChangeComplete | `((open: boolean) => void)` | - | Event handler called after any animations complete when the navigation menu is closed. |
| delay | `number` | `50` | How long to wait before opening the navigation menu. Specified in milliseconds. |
| closeDelay | `number` | `50` | How long to wait before closing the navigation menu. Specified in milliseconds. |
| orientation | `'horizontal' \| 'vertical'` | `'horizontal'` | The orientation of the navigation menu. |
| className | `string \| ((state: NavigationMenu.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: NavigationMenu.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: NavigationMenu.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### List
Contains a list of navigation menu items.
Renders a `<ul>` element.
**List Props:**
| Prop | Type | Default | Description |
| :-------- | :--------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: NavigationMenu.List.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: NavigationMenu.List.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: NavigationMenu.List.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Item
An individual navigation menu item.
Renders a `<li>` element.
**Item Props:**
| Prop | Type | Default | Description |
| :-------- | :--------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| value | `any` | - | A unique value that identifies this navigation menu item.
If no value is provided, a unique ID will be generated automatically.
Use when controlling the navigation menu programmatically. |
| className | `string \| ((state: NavigationMenu.Item.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: NavigationMenu.Item.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: NavigationMenu.Item.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Trigger
Opens the navigation menu popup when hovered or clicked, revealing the
associated content.
Renders a `<button>` element.
**Trigger Props:**
| Prop | Type | Default | Description |
| :----------- | :------------------------------------------------------------------------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| nativeButton | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it
via the `render` prop.
Set to `false` if the rendered element is not a button (e.g. `<div>`). |
| className | `string \| ((state: NavigationMenu.Trigger.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: NavigationMenu.Trigger.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: NavigationMenu.Trigger.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Trigger Data Attributes:**
| Attribute | Type | Description |
| :-------------- | :--- | :------------------------------------------------------ |
| data-popup-open | - | Present when the corresponding navigation menu is open. |
| data-pressed | - | Present when the trigger is pressed. |
### Icon
An icon that indicates that the trigger button opens a menu.
**Icon Props:**
| Prop | Type | Default | Description |
| :-------- | :--------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: NavigationMenu.Icon.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: NavigationMenu.Icon.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: NavigationMenu.Icon.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Content
A container for the content of the navigation menu item that is moved into the popup
when the item is active.
Renders a `<div>` element.
**Content Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: NavigationMenu.Content.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: NavigationMenu.Content.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: NavigationMenu.Content.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Content Data Attributes:**
| Attribute | Type | Description |
| :------------------------ | :--- | :-------------------------------------------------- |
| data-open | - | Present when the popup is open. |
| data-closed | - | Present when the popup is closed. |
| data-activation-direction | - | Which direction another trigger was activated from. |
| data-starting-style | - | Present when the content is animating in. |
| data-ending-style | - | Present when the content is animating out. |
### Link
A link in the navigation menu that can be used to navigate to a different page or section.
Renders an `<a>` element.
**Link Props:**
| Prop | Type | Default | Description |
| :----------- | :--------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| closeOnClick | `boolean` | `false` | Whether to close the navigation menu when the link is clicked. |
| active | `boolean` | `false` | Whether the link is the currently active page. |
| className | `string \| ((state: NavigationMenu.Link.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: NavigationMenu.Link.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: NavigationMenu.Link.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Link Data Attributes:**
| Attribute | Type | Description |
| :---------- | :--- | :-------------------------------------------------- |
| data-active | - | Present when the link is the currently active page. |
### Backdrop
A backdrop for the navigation menu popup.
Renders a `<div>` element.
**Backdrop Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: NavigationMenu.Backdrop.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: NavigationMenu.Backdrop.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: NavigationMenu.Backdrop.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Backdrop Data Attributes:**
| Attribute | Type | Description |
| :------------------ | :--- | :--------------------------------------- |
| data-open | - | Present when the popup is open. |
| data-closed | - | Present when the popup is closed. |
| data-starting-style | - | Present when the popup is animating in. |
| data-ending-style | - | Present when the popup is animating out. |
### Portal
A portal element that moves the popup to a different part of the DOM.
By default, the portal element is appended to `<body>`.
Renders a `<div>` element.
**Portal Props:**
| Prop | Type | Default | Description |
| :---------- | :----------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| container | `HTMLElement \| ShadowRoot \| RefObject<HTMLElement \| ShadowRoot \| null> \| null` | - | A parent element to render the portal element into. |
| className | `string \| ((state: NavigationMenu.Portal.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: NavigationMenu.Portal.State) => CSSProperties \| undefined)` | - | - |
| keepMounted | `boolean` | `false` | Whether to keep the portal mounted in the DOM while the popup is hidden. |
| render | `ReactElement \| ((props: HTMLProps, state: NavigationMenu.Portal.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Positioner
Positions the navigation menu against the currently active trigger.
Renders a `<div>` element.
**Positioner Props:**
| Prop | Type | Default | Description |
| :-------------------- | :------------------------- | :--------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| disableAnchorTracking | `boolean` | `false` | Whether to disable the popup from tracking any layout shift of its positioning anchor. |
| align | `Align` | `'center'` | How to align the popup relative to the specified side. |
| alignOffset | `number \| OffsetFunction` | `0` | Additional offset along the alignment axis in pixels.
Also accepts a function that returns the offset to read the dimensions of the anchor
and positioner elements, along with its side and alignment.The function takes a `data` object parameter with the following properties:\* `data.anchor`: the dimensions of the anchor element with properties `width` and `height`. |
- `data.positioner`: the dimensions of the positioner element with properties `width` and `height`.
- `data.side`: which side of the anchor element the positioner is aligned against.
- `data.align`: how the positioner is aligned relative to the specified side. |
| side | `Side` | `'bottom'` | Which side of the anchor element to align the popup against.
May automatically change to avoid collisions. |
| sideOffset | `number \| OffsetFunction` | `0` | Distance between the anchor and the popup in pixels.
Also accepts a function that returns the distance to read the dimensions of the anchor
and positioner elements, along with its side and alignment.The function takes a `data` object parameter with the following properties:\* `data.anchor`: the dimensions of the anchor element with properties `width` and `height`.
- `data.positioner`: the dimensions of the positioner element with properties `width` and `height`.
- `data.side`: which side of the anchor element the positioner is aligned against.
- `data.align`: how the positioner is aligned relative to the specified side. |
| arrowPadding | `number` | `5` | Minimum distance to maintain between the arrow and the edges of the popup.Use it to prevent the arrow element from hanging out of the rounded corners of a popup. |
| anchor | `Element \| RefObject<Element \| null> \| VirtualElement \| (() => Element \| VirtualElement \| null) \| null` | - | An element to position the popup against.
By default, the popup will be positioned against the trigger. |
| collisionAvoidance | `CollisionAvoidance` | - | Determines how to handle collisions when positioning the popup. |
| collisionBoundary | `Boundary` | `'clipping-ancestors'` | An element or a rectangle that delimits the area that the popup is confined to. |
| collisionPadding | `Padding` | `5` | Additional space to maintain from the edge of the collision boundary. |
| sticky | `boolean` | `false` | Whether to maintain the popup in the viewport after
the anchor element was scrolled out of view. |
| positionMethod | `'fixed' \| 'absolute'` | `'absolute'` | Determines which CSS `position` property to use. |
| className | `string \| ((state: NavigationMenu.Positioner.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: NavigationMenu.Positioner.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: NavigationMenu.Positioner.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Positioner Data Attributes:**
| Attribute | Type | Description |
| :----------------- | :------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| data-open | - | Present when the popup is open. |
| data-closed | - | Present when the popup is closed. |
| data-anchor-hidden | - | Present when the anchor is hidden. |
| data-align | `'start' \| 'center' \| 'end'` | Indicates how the popup is aligned relative to the specified side. |
| data-instant | - | Present if animations should be instant. |
| data-side | `'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the popup is positioned relative to the trigger. |
**Positioner CSS Variables:**
| Variable | Type | Default | Description |
| :------------------ | :------- | :------ | :------------------------------------------------------------------------------------- |
| --anchor-height | `number` | - | The anchor's height. |
| --anchor-width | `number` | - | The anchor's width. |
| --available-height | `number` | - | The available height between the trigger and the edge of the viewport. |
| --available-width | `number` | - | The available width between the trigger and the edge of the viewport. |
| --positioner-height | `number` | - | The fixed height of the positioner element. |
| --positioner-width | `number` | - | The fixed width of the positioner element. |
| --transform-origin | `string` | - | The coordinates that this element is anchored to. Used for animations and transitions. |
### Popup
A container for the navigation menu contents.
Renders a `<nav>` element.
**Popup Props:**
| Prop | Type | Default | Description |
| :-------- | :---------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: NavigationMenu.Popup.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: NavigationMenu.Popup.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: NavigationMenu.Popup.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Popup Data Attributes:**
| Attribute | Type | Description |
| :------------------ | :------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| data-open | - | Present when the popup is open. |
| data-closed | - | Present when the popup is closed. |
| data-align | `'start' \| 'center' \| 'end'` | Indicates how the popup is aligned relative to the specified side. |
| data-side | `'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the popup is positioned relative to the trigger. |
| data-starting-style | - | Present when the popup is animating in. |
| data-ending-style | - | Present when the popup is animating out. |
**Popup CSS Variables:**
| Variable | Type | Default | Description |
| :------------- | :------- | :------ | :------------------------------------- |
| --popup-height | `number` | - | The fixed height of the popup element. |
| --popup-width | `number` | - | The fixed width of the popup element. |
### Viewport
The clipping viewport of the navigation menu's current content.
Renders a `<div>` element.
**Viewport Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: NavigationMenu.Viewport.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: NavigationMenu.Viewport.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: NavigationMenu.Viewport.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Arrow
Displays an element pointing toward the navigation menu's current anchor.
Renders a `<div>` element.
**Arrow Props:**
| Prop | Type | Default | Description |
| :-------- | :---------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: NavigationMenu.Arrow.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: NavigationMenu.Arrow.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: NavigationMenu.Arrow.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Arrow Data Attributes:**
| Attribute | Type | Description |
| :-------------- | :------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| data-open | - | Present when the popup is open. |
| data-closed | - | Present when the popup is closed. |
| data-uncentered | - | Present when the popup arrow is uncentered. |
| data-align | `'start' \| 'center' \| 'end'` | Indicates how the popup is aligned relative to specified side. |
| data-side | `'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the popup is positioned relative to the trigger. |
references/number-field.md
---
title: Number Field
subtitle: A numeric input element with increment and decrement buttons, and a scrub area.
description: A high-quality, unstyled React number field component with increment and decrement buttons, and a scrub area.
---
# Number Field
A high-quality, unstyled React number field component with increment and decrement buttons, and a scrub area.
## v1.9.0 notes
- `<NumberField.ScrubArea>` fires `onClick` once per click, so a click handler no longer runs twice for a single interaction.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import * as React from "react";
import { NumberField } from "@base-ui/react/number-field";
export default function ExampleNumberField() {
const id = React.useId();
return (
<NumberField.Root id={id} defaultValue={100} className="flex flex-col items-start gap-1">
<NumberField.ScrubArea className="cursor-ew-resize">
<label htmlFor={id} className="cursor-ew-resize text-sm font-medium text-gray-900">
Amount
</label>
<NumberField.ScrubAreaCursor className="drop-shadow-[0_1px_1px_#0008] filter">
<CursorGrowIcon />
</NumberField.ScrubAreaCursor>
</NumberField.ScrubArea>
<NumberField.Group className="flex">
<NumberField.Decrement className="flex size-10 items-center justify-center rounded-tl-md rounded-bl-md border border-gray-200 bg-gray-50 bg-clip-padding text-gray-900 select-none hover:bg-gray-100 active:bg-gray-100">
<MinusIcon />
</NumberField.Decrement>
<NumberField.Input className="h-10 w-24 border-t border-b border-gray-200 text-center text-base text-gray-900 tabular-nums focus:z-1 focus:outline focus:outline-2 focus:-outline-offset-1 focus:outline-blue-800" />
<NumberField.Increment className="flex size-10 items-center justify-center rounded-tr-md rounded-br-md border border-gray-200 bg-gray-50 bg-clip-padding text-gray-900 select-none hover:bg-gray-100 active:bg-gray-100">
<PlusIcon />
</NumberField.Increment>
</NumberField.Group>
</NumberField.Root>
);
}
function CursorGrowIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="26" height="14" viewBox="0 0 24 14" fill="black" stroke="white" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M19.5 5.5L6.49737 5.51844V2L1 6.9999L6.5 12L6.49737 8.5L19.5 8.5V12L25 6.9999L19.5 2V5.5Z" />
</svg>
);
}
function PlusIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentcolor" strokeWidth="1.6" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M0 5H5M10 5H5M5 5V0M5 5V10" />
</svg>
);
}
function MinusIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentcolor" strokeWidth="1.6" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M0 5H10" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Field {
display: flex;
flex-direction: column;
align-items: start;
gap: 0.25rem;
}
.ScrubArea {
cursor: ew-resize;
font-weight: bold;
user-select: none;
}
.ScrubAreaCursor {
filter: drop-shadow(0 1px 1px #0008);
}
.Label {
cursor: ew-resize;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
color: var(--color-gray-900);
}
.Group {
display: flex;
}
.Input {
box-sizing: border-box;
margin: 0;
padding: 0;
border-radius: 0;
border-top: 1px solid var(--color-gray-200);
border-bottom: 1px solid var(--color-gray-200);
border-left: none;
border-right: none;
width: 6rem;
height: 2.5rem;
font-family: inherit;
font-size: 1rem;
font-weight: normal;
background-color: transparent;
color: var(--color-gray-900);
text-align: center;
font-variant-numeric: tabular-nums;
&:focus {
z-index: 1;
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Decrement,
.Increment {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
margin: 0;
outline: 0;
padding: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
background-clip: padding-box;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
}
.Decrement {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.Increment {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
```
```tsx
/* index.tsx */
import * as React from "react";
import { NumberField } from "@base-ui/react/number-field";
import styles from "./index.module.css";
export default function ExampleNumberField() {
const id = React.useId();
return (
<NumberField.Root id={id} defaultValue={100} className={styles.Field}>
<NumberField.ScrubArea className={styles.ScrubArea}>
<label htmlFor={id} className={styles.Label}>
Amount
</label>
<NumberField.ScrubAreaCursor className={styles.ScrubAreaCursor}>
<CursorGrowIcon />
</NumberField.ScrubAreaCursor>
</NumberField.ScrubArea>
<NumberField.Group className={styles.Group}>
<NumberField.Decrement className={styles.Decrement}>
<MinusIcon />
</NumberField.Decrement>
<NumberField.Input className={styles.Input} />
<NumberField.Increment className={styles.Increment}>
<PlusIcon />
</NumberField.Increment>
</NumberField.Group>
</NumberField.Root>
);
}
function CursorGrowIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="26" height="14" viewBox="0 0 24 14" fill="black" stroke="white" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M19.5 5.5L6.49737 5.51844V2L1 6.9999L6.5 12L6.49737 8.5L19.5 8.5V12L25 6.9999L19.5 2V5.5Z" />
</svg>
);
}
function PlusIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentcolor" strokeWidth="1.6" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M0 5H5M10 5H5M5 5V0M5 5V10" />
</svg>
);
}
function MinusIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentcolor" strokeWidth="1.6" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M0 5H10" />
</svg>
);
}
```
## Usage guidelines
- **Form controls must have an accessible name**: It can be created using a `<label>` element or the `Field` component. See the [forms guide](/react/handbook/forms.md).
## Anatomy
Import the component and assemble its parts:
```jsx title="Anatomy"
import { NumberField } from "@base-ui/react/number-field";
<NumberField.Root>
<NumberField.ScrubArea>
<NumberField.ScrubAreaCursor />
</NumberField.ScrubArea>
<NumberField.Group>
<NumberField.Decrement />
<NumberField.Input />
<NumberField.Increment />
</NumberField.Group>
</NumberField.Root>;
```
## API reference
### Root
Groups all parts of the number field and manages its state.
Renders a `<div>` element.
**Root Props:**
| Prop | Type | Default | Description |
| :------------ | :------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | `string` | - | Identifies the field when a form is submitted. |
| defaultValue | `number` | - | The uncontrolled value of the field when it’s initially rendered. To render a controlled number field, use the `value` prop instead. |
| value | `number \| null` | - | The raw numeric value of the field. |
| onValueChange | `((value: number \| null, eventDetails: NumberField.Root.ChangeEventDetails) => void)` | - | Callback fired when the number value changes.The `eventDetails.reason` indicates what triggered the change:\* `'input-change'` for parseable typing or programmatic text updates |
- `'input-clear'` when the field becomes empty
- `'input-blur'` when formatting or clamping occurs on blur
- `'input-paste'` for paste interactions
- `'keyboard'` for keyboard input
- `'increment-press'` / `'decrement-press'` for button presses on the increment and decrement controls
- `'wheel'` for wheel-based scrubbing
- `'scrub'` for scrub area drags |
| onValueCommitted | `((value: number \| null, eventDetails: NumberField.Root.CommitEventDetails) => void)` | - | Callback function that is fired when the value is committed.
It runs later than `onValueChange`, when:\* The input is blurred after typing a value.
- The pointer is released after scrubbing or pressing the increment/decrement buttons.It runs simultaneously with `onValueChange` when interacting with the keyboard.**Warning**: This is a generic event not a change event. |
| locale | `Intl.LocalesArgument` | - | The locale of the input element.
Defaults to the user's runtime locale. |
| snapOnStep | `boolean` | `false` | Whether the value should snap to the nearest step when incrementing or decrementing. |
| step | `number \| 'any'` | `1` | Amount to increment and decrement with the buttons and arrow keys, or to scrub with pointer movement in the scrub area.
To always enable step validation on form submission, specify the `min` prop explicitly in conjunction with this prop.
Specify `step="any"` to always disable step validation. |
| smallStep | `number` | `0.1` | The small step value of the input element when incrementing while the meta key is held. Snaps
to multiples of this value. |
| largeStep | `number` | `10` | The large step value of the input element when incrementing while the shift key is held. Snaps
to multiples of this value. |
| min | `number` | - | The minimum value of the input element. |
| max | `number` | - | The maximum value of the input element. |
| allowWheelScrub | `boolean` | `false` | Whether to allow the user to scrub the input value with the mouse wheel while focused and
hovering over the input. |
| format | `Intl.NumberFormatOptions` | - | Options to format the input value. |
| disabled | `boolean` | `false` | Whether the component should ignore user interaction. |
| readOnly | `boolean` | `false` | Whether the user should be unable to change the field value. |
| required | `boolean` | `false` | Whether the user must enter a value before submitting a form. |
| inputRef | `Ref<HTMLInputElement>` | - | A ref to access the hidden input element. |
| id | `string` | - | The id of the input element. |
| className | `string \| ((state: NumberField.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: NumberField.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: NumberField.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Root Data Attributes:**
| Attribute | Type | Description |
| :------------- | :--- | :------------------------------------------------------------------------------ |
| data-disabled | - | Present when the number field is disabled. |
| data-readonly | - | Present when the number field is readonly. |
| data-required | - | Present when the number field is required. |
| data-valid | - | Present when the number field is in valid state (when wrapped in Field.Root). |
| data-invalid | - | Present when the number field is in invalid state (when wrapped in Field.Root). |
| data-dirty | - | Present when the number field's value has changed (when wrapped in Field.Root). |
| data-touched | - | Present when the number field has been touched (when wrapped in Field.Root). |
| data-filled | - | Present when the number field is filled (when wrapped in Field.Root). |
| data-focused | - | Present when the number field is focused (when wrapped in Field.Root). |
| data-scrubbing | - | Present while scrubbing. |
### ScrubArea
An interactive area where the user can click and drag to change the field value.
Renders a `<span>` element.
**ScrubArea Props:**
| Prop | Type | Default | Description |
| :--------------- | :----------------------------------------------------------------------------------------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| direction | `'horizontal' \| 'vertical'` | `'horizontal'` | Cursor movement direction in the scrub area. |
| pixelSensitivity | `number` | `2` | Determines how many pixels the cursor must move before the value changes.
A higher value will make scrubbing less sensitive. |
| teleportDistance | `number` | - | If specified, determines the distance that the cursor may move from the center
of the scrub area before it will loop back around. |
| className | `string \| ((state: NumberField.ScrubArea.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: NumberField.ScrubArea.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: NumberField.ScrubArea.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**ScrubArea Data Attributes:**
| Attribute | Type | Description |
| :------------- | :--- | :------------------------------------------------------------------------------ |
| data-disabled | - | Present when the number field is disabled. |
| data-readonly | - | Present when the number field is readonly. |
| data-required | - | Present when the number field is required. |
| data-valid | - | Present when the number field is in valid state (when wrapped in Field.Root). |
| data-invalid | - | Present when the number field is in invalid state (when wrapped in Field.Root). |
| data-dirty | - | Present when the number field's value has changed (when wrapped in Field.Root). |
| data-touched | - | Present when the number field has been touched (when wrapped in Field.Root). |
| data-filled | - | Present when the number field is filled (when wrapped in Field.Root). |
| data-focused | - | Present when the number field is focused (when wrapped in Field.Root). |
| data-scrubbing | - | Present while scrubbing. |
### ScrubAreaCursor
A custom element to display instead of the native cursor while using the scrub area.
Renders a `<span>` element.This component uses the [Pointer Lock API](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API), which may prompt the browser to display a related notification. It is disabled
in Safari to avoid a layout shift that this notification causes there.
**ScrubAreaCursor Props:**
| Prop | Type | Default | Description |
| :-------- | :----------------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: NumberField.ScrubAreaCursor.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: NumberField.ScrubAreaCursor.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: NumberField.ScrubAreaCursor.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**ScrubAreaCursor Data Attributes:**
| Attribute | Type | Description |
| :------------- | :--- | :------------------------------------------------------------------------------ |
| data-disabled | - | Present when the number field is disabled. |
| data-readonly | - | Present when the number field is readonly. |
| data-required | - | Present when the number field is required. |
| data-valid | - | Present when the number field is in valid state (when wrapped in Field.Root). |
| data-invalid | - | Present when the number field is in invalid state (when wrapped in Field.Root). |
| data-dirty | - | Present when the number field's value has changed (when wrapped in Field.Root). |
| data-touched | - | Present when the number field has been touched (when wrapped in Field.Root). |
| data-filled | - | Present when the number field is filled (when wrapped in Field.Root). |
| data-focused | - | Present when the number field is focused (when wrapped in Field.Root). |
| data-scrubbing | - | Present while scrubbing. |
### Group
Groups the input with the increment and decrement buttons.
Renders a `<div>` element.
**Group Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: NumberField.Group.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: NumberField.Group.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: NumberField.Group.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Group Data Attributes:**
| Attribute | Type | Description |
| :------------- | :--- | :------------------------------------------------------------------------------ |
| data-disabled | - | Present when the number field is disabled. |
| data-readonly | - | Present when the number field is readonly. |
| data-required | - | Present when the number field is required. |
| data-valid | - | Present when the number field is in valid state (when wrapped in Field.Root). |
| data-invalid | - | Present when the number field is in invalid state (when wrapped in Field.Root). |
| data-dirty | - | Present when the number field's value has changed (when wrapped in Field.Root). |
| data-touched | - | Present when the number field has been touched (when wrapped in Field.Root). |
| data-filled | - | Present when the number field is filled (when wrapped in Field.Root). |
| data-focused | - | Present when the number field is focused (when wrapped in Field.Root). |
| data-scrubbing | - | Present while scrubbing. |
### Decrement
A stepper button that decreases the field value when clicked.
Renders an `<button>` element.
**Decrement Props:**
| Prop | Type | Default | Description |
| :----------- | :----------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| nativeButton | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it
via the `render` prop.
Set to `false` if the rendered element is not a button (e.g. `<div>`). |
| className | `string \| ((state: NumberField.Decrement.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: NumberField.Decrement.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: NumberField.Decrement.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Decrement Data Attributes:**
| Attribute | Type | Description |
| :------------- | :--- | :------------------------------------------------------------------------------ |
| data-disabled | - | Present when the number field is disabled. |
| data-readonly | - | Present when the number field is readonly. |
| data-required | - | Present when the number field is required. |
| data-valid | - | Present when the number field is in valid state (when wrapped in Field.Root). |
| data-invalid | - | Present when the number field is in invalid state (when wrapped in Field.Root). |
| data-dirty | - | Present when the number field's value has changed (when wrapped in Field.Root). |
| data-touched | - | Present when the number field has been touched (when wrapped in Field.Root). |
| data-filled | - | Present when the number field is filled (when wrapped in Field.Root). |
| data-focused | - | Present when the number field is focused (when wrapped in Field.Root). |
| data-scrubbing | - | Present while scrubbing. |
### Input
The native input control in the number field.
Renders an `<input>` element.
**Input Props:**
| Prop | Type | Default | Description |
| :------------------- | :------------------------------------------------------------------------------------- | :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| aria-roledescription | `string` | `'Number field'` | A string value that provides a user-friendly name for the role of the input. |
| className | `string \| ((state: NumberField.Input.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: NumberField.Input.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: NumberField.Input.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Input Data Attributes:**
| Attribute | Type | Description |
| :------------- | :--- | :------------------------------------------------------------------------------ |
| data-disabled | - | Present when the number field is disabled. |
| data-readonly | - | Present when the number field is readonly. |
| data-required | - | Present when the number field is required. |
| data-valid | - | Present when the number field is in valid state (when wrapped in Field.Root). |
| data-invalid | - | Present when the number field is in invalid state (when wrapped in Field.Root). |
| data-dirty | - | Present when the number field's value has changed (when wrapped in Field.Root). |
| data-touched | - | Present when the number field has been touched (when wrapped in Field.Root). |
| data-filled | - | Present when the number field is filled (when wrapped in Field.Root). |
| data-focused | - | Present when the number field is focused (when wrapped in Field.Root). |
| data-scrubbing | - | Present while scrubbing. |
### Increment
A stepper button that increases the field value when clicked.
Renders an `<button>` element.
**Increment Props:**
| Prop | Type | Default | Description |
| :----------- | :----------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| nativeButton | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it
via the `render` prop.
Set to `false` if the rendered element is not a button (e.g. `<div>`). |
| className | `string \| ((state: NumberField.Increment.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: NumberField.Increment.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: NumberField.Increment.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Increment Data Attributes:**
| Attribute | Type | Description |
| :------------- | :--- | :------------------------------------------------------------------------------ |
| data-disabled | - | Present when the number field is disabled. |
| data-readonly | - | Present when the number field is readonly. |
| data-required | - | Present when the number field is required. |
| data-valid | - | Present when the number field is in valid state (when wrapped in Field.Root). |
| data-invalid | - | Present when the number field is in invalid state (when wrapped in Field.Root). |
| data-dirty | - | Present when the number field's value has changed (when wrapped in Field.Root). |
| data-touched | - | Present when the number field has been touched (when wrapped in Field.Root). |
| data-filled | - | Present when the number field is filled (when wrapped in Field.Root). |
| data-focused | - | Present when the number field is focused (when wrapped in Field.Root). |
| data-scrubbing | - | Present while scrubbing. |
references/otp-field.md
# OTP Field
Base UI `OTPField` is a preview primitive for one-time-password and verification-code entry.
## Import and anatomy
```tsx
import { OTPFieldPreview as OTPField } from "@base-ui/react/otp-field";
<OTPField.Root length={6}>
<OTPField.Input />
<OTPField.Separator />
</OTPField.Root>;
```
## Core parts
- `OTPField.Root` manages the overall value, validation, and form integration.
- `OTPField.Input` renders each character slot.
- `OTPField.Separator` is a visual grouping element for layouts such as `123-456`.
## Practical guidance
- Treat it as preview API surface until Base UI marks it stable.
- Always provide an accessible name via a native `<label>` or `Field` composition.
- Use `validationType="numeric"` for standard verification codes; switch to `alphanumeric` only when the backend really expects mixed characters.
- Use `onValueComplete` for side effects like verification requests, and `autoSubmit` only when the owning form can safely submit as soon as the value is complete.
- For custom input cleanup rules, set `validationType="none"` and provide `normalizeValue`.
## v1.5.0 notes
- `sanitizeValue()` was renamed to `normalizeValue()`.
- Keyboard shortcuts such as Ctrl/Cmd editing behave more reliably.
- Prefer one normalization path plus explicit validation instead of layering custom sanitizers.
## v1.9.0 notes
- Safari IME composition no longer over-fills slots, so an in-progress composition no longer pushes characters into the next slot.
## Operational notes
- `autoComplete="one-time-code"` is the default and should usually be preserved for mobile autofill flows.
- `form`, `required`, `readOnly`, `disabled`, and `name` live on `Root`, so form semantics stay centralized.
- `mask` is useful for short-lived verification secrets, but verify that the UX still supports paste and correction clearly.
references/popover.md
---
title: Popover
subtitle: An accessible popup anchored to a button.
description: A high-quality, unstyled React popover component that displays an accessible popup anchored to a button.
---
# Popover
A high-quality, unstyled React popover component that displays an accessible popup anchored to a button.
## v1.4.1 notes
- Popup sizing was fixed for rapid trigger-hover transitions. If you previously added manual re-measure hacks for hover-driven overlays, retest before keeping them.
## v1.5.0 notes
- Controlled `open` state is respected more consistently for popup-open detection. Re-test any styling or side effects keyed off `data-[popup-open]` before keeping older synchronization workarounds.
- RTL positioning is corrected in the `1.5.0` line.
## v1.3.0 notes
- Modal popovers with an internal `Popover.Close` now trap focus more reliably, including cases with visually hidden close buttons.
- Nested hoverable popup behavior was hardened again; if you compose popovers with preview cards or menus, retest pointer transitions instead of assuming older hover bugs still apply.
## v1.9.0 notes
- Automatic unmounting is decoupled from `actionsRef`. Call `eventDetails.preventUnmountOnClose()` in `onOpenChange` to opt out, then end the closing phase with `actionsRef.current.unmount()`. See the Unmount control section below.
- The focus guard no longer loops when the trigger is the only tabbable element.
- Hover-out animations are fixed when switching between detached triggers, and a viewport transition no longer starts when a retained popup reopens.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import { Popover } from "@base-ui/react/popover";
import { BellIcon, ArrowSvg } from "./icons-tw";
export default function ExamplePopover() {
return (
<Popover.Root>
<Popover.Trigger className="flex size-10 items-center justify-center rounded-md border border-gray-200 bg-gray-50 text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100 data-[popup-open]:bg-gray-100">
<BellIcon aria-label="Notifications" />
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner sideOffset={8}>
<Popover.Popup className="origin-[var(--transform-origin)] rounded-lg bg-[canvas] px-6 py-4 text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0 dark:shadow-none dark:-outline-offset-1 dark:outline-gray-300">
<Popover.Arrow className="data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180">
<ArrowSvg />
</Popover.Arrow>
<Popover.Title className="text-base font-medium">Notifications</Popover.Title>
<Popover.Description className="text-base text-gray-600">You are all caught up. Good job!</Popover.Description>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}
```
```tsx
/* icons-tw.tsx */
import * as React from "react";
export function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className="fill-[canvas]" />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className="fill-gray-200 dark:fill-none" />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className="dark:fill-gray-300" />
</svg>
);
}
export function BellIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="20" height="20" viewBox="0 0 16 16" {...props}>
<path d="M 8 1 C 7.453125 1 7 1.453125 7 2 L 7 3.140625 C 5.28125 3.589844 4 5.144531 4 7 L 4 10.984375 C 4 10.984375 3.984375 11.261719 3.851563 11.519531 C 3.71875 11.78125 3.558594 12 3 12 L 3 13 L 13 13 L 13 12 C 12.40625 12 12.253906 11.78125 12.128906 11.53125 C 12.003906 11.277344 12 11.003906 12 11.003906 L 12 7 C 12 5.144531 10.71875 3.589844 9 3.140625 L 9 2 C 9 1.453125 8.546875 1 8 1 Z M 8 13 C 7.449219 13 7 13.449219 7 14 C 7 14.550781 7.449219 15 8 15 C 8.550781 15 9 14.550781 9 14 C 9 13.449219 8.550781 13 8 13 Z M 8 4 C 9.664063 4 11 5.335938 11 7 L 11 10.996094 C 11 10.996094 10.988281 11.472656 11.234375 11.96875 C 11.238281 11.980469 11.246094 11.988281 11.25 12 L 4.726563 12 C 4.730469 11.992188 4.738281 11.984375 4.742188 11.980469 C 4.992188 11.488281 5 11.015625 5 11.015625 L 5 7 C 5 5.335938 6.335938 4 8 4 Z" />
</svg>
);
}
export function UserIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M18 20a6 6 0 0 0-12 0" />
<circle cx="12" cy="10" r="4" />
<circle cx="12" cy="12" r="10" />
</svg>
);
}
export function ListIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M3 5h.01" />
<path d="M3 12h.01" />
<path d="M3 19h.01" />
<path d="M8 5h13" />
<path d="M8 12h13" />
<path d="M8 19h13" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.IconButton {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
padding: 0;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Icon {
width: 1.25rem;
height: 1.25rem;
}
.Positioner {
width: var(--positioner-width);
height: var(--positioner-height);
max-width: var(--available-width);
}
.Popup {
box-sizing: border-box;
padding: 1rem 1.5rem;
border-radius: 0.5rem;
background-color: canvas;
color: var(--color-gray-900);
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
width: var(--popup-width, auto);
height: var(--popup-height, auto);
max-width: 500px;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.Title {
margin: 0;
font-size: 1rem;
line-height: 1.5rem;
font-weight: 500;
}
.Description {
margin: 0;
font-size: 1rem;
line-height: 1.5rem;
color: var(--color-gray-600);
}
.Container {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
}
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
```
```tsx
/* index.tsx */
import * as React from "react";
import { Popover } from "@base-ui/react/popover";
import styles from "./index.module.css";
export default function ExamplePopover() {
return (
<Popover.Root>
<Popover.Trigger className={styles.IconButton}>
<BellIcon aria-label="Notifications" className={styles.Icon} />
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner sideOffset={8}>
<Popover.Popup className={styles.Popup}>
<Popover.Arrow className={styles.Arrow}>
<ArrowSvg />
</Popover.Arrow>
<Popover.Title className={styles.Title}>Notifications</Popover.Title>
<Popover.Description className={styles.Description}>You are all caught up. Good job!</Popover.Description>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className={styles.ArrowFill} />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className={styles.ArrowOuterStroke} />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className={styles.ArrowInnerStroke} />
</svg>
);
}
function BellIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="20" height="20" viewBox="0 0 16 16" {...props}>
<path d="M 8 1 C 7.453125 1 7 1.453125 7 2 L 7 3.140625 C 5.28125 3.589844 4 5.144531 4 7 L 4 10.984375 C 4 10.984375 3.984375 11.261719 3.851563 11.519531 C 3.71875 11.78125 3.558594 12 3 12 L 3 13 L 13 13 L 13 12 C 12.40625 12 12.253906 11.78125 12.128906 11.53125 C 12.003906 11.277344 12 11.003906 12 11.003906 L 12 7 C 12 5.144531 10.71875 3.589844 9 3.140625 L 9 2 C 9 1.453125 8.546875 1 8 1 Z M 8 13 C 7.449219 13 7 13.449219 7 14 C 7 14.550781 7.449219 15 8 15 C 8.550781 15 9 14.550781 9 14 C 9 13.449219 8.550781 13 8 13 Z M 8 4 C 9.664063 4 11 5.335938 11 7 L 11 10.996094 C 11 10.996094 10.988281 11.472656 11.234375 11.96875 C 11.238281 11.980469 11.246094 11.988281 11.25 12 L 4.726563 12 C 4.730469 11.992188 4.738281 11.984375 4.742188 11.980469 C 4.992188 11.488281 5 11.015625 5 11.015625 L 5 7 C 5 5.335938 6.335938 4 8 4 Z" />
</svg>
);
}
```
## Anatomy
Import the component and assemble its parts:
```jsx title="Anatomy"
import { Popover } from "@base-ui/react/popover";
<Popover.Root>
<Popover.Trigger />
<Popover.Portal>
<Popover.Backdrop />
<Popover.Positioner>
<Popover.Popup>
<Popover.Arrow />
<Popover.Viewport>
<Popover.Title />
<Popover.Description />
<Popover.Close />
</Popover.Viewport>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>;
```
## Examples
### Opening on hover
This example shows how you can configure the popover to open on hover using the `openOnHover` prop.
You can use the `delay` prop to specify how long to wait (in milliseconds) before the popover opens on hover.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import { Popover } from "@base-ui/react/popover";
import { BellIcon, ArrowSvg } from "./icons-tw";
export default function ExamplePopover() {
return (
<Popover.Root>
<Popover.Trigger openOnHover className="flex size-10 items-center justify-center rounded-md border border-gray-200 bg-gray-50 text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100 data-[popup-open]:bg-gray-100">
<BellIcon aria-label="Notifications" />
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner sideOffset={8}>
<Popover.Popup className="origin-[var(--transform-origin)] rounded-lg bg-[canvas] px-6 py-4 text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0 dark:shadow-none dark:-outline-offset-1 dark:outline-gray-300">
<Popover.Arrow className="data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180">
<ArrowSvg />
</Popover.Arrow>
<Popover.Title className="text-base font-medium">Notifications</Popover.Title>
<Popover.Description className="text-base text-gray-600">You are all caught up. Good job!</Popover.Description>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}
```
```tsx
/* icons-tw.tsx */
import * as React from "react";
export function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className="fill-[canvas]" />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className="fill-gray-200 dark:fill-none" />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className="dark:fill-gray-300" />
</svg>
);
}
export function BellIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="20" height="20" viewBox="0 0 16 16" {...props}>
<path d="M 8 1 C 7.453125 1 7 1.453125 7 2 L 7 3.140625 C 5.28125 3.589844 4 5.144531 4 7 L 4 10.984375 C 4 10.984375 3.984375 11.261719 3.851563 11.519531 C 3.71875 11.78125 3.558594 12 3 12 L 3 13 L 13 13 L 13 12 C 12.40625 12 12.253906 11.78125 12.128906 11.53125 C 12.003906 11.277344 12 11.003906 12 11.003906 L 12 7 C 12 5.144531 10.71875 3.589844 9 3.140625 L 9 2 C 9 1.453125 8.546875 1 8 1 Z M 8 13 C 7.449219 13 7 13.449219 7 14 C 7 14.550781 7.449219 15 8 15 C 8.550781 15 9 14.550781 9 14 C 9 13.449219 8.550781 13 8 13 Z M 8 4 C 9.664063 4 11 5.335938 11 7 L 11 10.996094 C 11 10.996094 10.988281 11.472656 11.234375 11.96875 C 11.238281 11.980469 11.246094 11.988281 11.25 12 L 4.726563 12 C 4.730469 11.992188 4.738281 11.984375 4.742188 11.980469 C 4.992188 11.488281 5 11.015625 5 11.015625 L 5 7 C 5 5.335938 6.335938 4 8 4 Z" />
</svg>
);
}
export function UserIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M18 20a6 6 0 0 0-12 0" />
<circle cx="12" cy="10" r="4" />
<circle cx="12" cy="12" r="10" />
</svg>
);
}
export function ListIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M3 5h.01" />
<path d="M3 12h.01" />
<path d="M3 19h.01" />
<path d="M8 5h13" />
<path d="M8 12h13" />
<path d="M8 19h13" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.IconButton {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
padding: 0;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Icon {
width: 1.25rem;
height: 1.25rem;
}
.Positioner {
width: var(--positioner-width);
height: var(--positioner-height);
max-width: var(--available-width);
}
.Popup {
box-sizing: border-box;
padding: 1rem 1.5rem;
border-radius: 0.5rem;
background-color: canvas;
color: var(--color-gray-900);
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
width: var(--popup-width, auto);
height: var(--popup-height, auto);
max-width: 500px;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.Title {
margin: 0;
font-size: 1rem;
line-height: 1.5rem;
font-weight: 500;
}
.Description {
margin: 0;
font-size: 1rem;
line-height: 1.5rem;
color: var(--color-gray-600);
}
.Container {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
}
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
```
```tsx
/* index.tsx */
import * as React from "react";
import { Popover } from "@base-ui/react/popover";
import styles from "./index.module.css";
export default function ExamplePopover() {
return (
<Popover.Root>
<Popover.Trigger openOnHover className={styles.IconButton}>
<BellIcon aria-label="Notifications" className={styles.Icon} />
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner sideOffset={8}>
<Popover.Popup className={styles.Popup}>
<Popover.Arrow className={styles.Arrow}>
<ArrowSvg />
</Popover.Arrow>
<Popover.Title className={styles.Title}>Notifications</Popover.Title>
<Popover.Description className={styles.Description}>You are all caught up. Good job!</Popover.Description>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className={styles.ArrowFill} />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className={styles.ArrowOuterStroke} />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className={styles.ArrowInnerStroke} />
</svg>
);
}
function BellIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="20" height="20" viewBox="0 0 16 16" {...props}>
<path d="M 8 1 C 7.453125 1 7 1.453125 7 2 L 7 3.140625 C 5.28125 3.589844 4 5.144531 4 7 L 4 10.984375 C 4 10.984375 3.984375 11.261719 3.851563 11.519531 C 3.71875 11.78125 3.558594 12 3 12 L 3 13 L 13 13 L 13 12 C 12.40625 12 12.253906 11.78125 12.128906 11.53125 C 12.003906 11.277344 12 11.003906 12 11.003906 L 12 7 C 12 5.144531 10.71875 3.589844 9 3.140625 L 9 2 C 9 1.453125 8.546875 1 8 1 Z M 8 13 C 7.449219 13 7 13.449219 7 14 C 7 14.550781 7.449219 15 8 15 C 8.550781 15 9 14.550781 9 14 C 9 13.449219 8.550781 13 8 13 Z M 8 4 C 9.664063 4 11 5.335938 11 7 L 11 10.996094 C 11 10.996094 10.988281 11.472656 11.234375 11.96875 C 11.238281 11.980469 11.246094 11.988281 11.25 12 L 4.726563 12 C 4.730469 11.992188 4.738281 11.984375 4.742188 11.980469 C 4.992188 11.488281 5 11.015625 5 11.015625 L 5 7 C 5 5.335938 6.335938 4 8 4 Z" />
</svg>
);
}
```
### Detached triggers
A popover can be controlled by a trigger located either inside or outside the `<Popover.Root>` component.
For simple, one-off interactions, place the `<Popover.Trigger>` inside `<Popover.Root>`, as shown in the example at the top of this page.
However, if defining the popover's content next to its trigger is not practical, you can use a detached trigger.
This involves placing the `<Popover.Trigger>` outside of `<Popover.Root>` and linking them with a `handle` created by the `Popover.createHandle()` function.
```jsx title="Detached triggers"
const demoPopover = Popover.createHandle();
<Popover.Trigger handle={demoPopover}>
Trigger
</Popover.Trigger>
<Popover.Root handle={demoPopover}>
...
</Popover.Root>
```
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Popover } from "@base-ui/react/popover";
import { ArrowSvg, BellIcon } from "./icons-tw";
const demoPopover = Popover.createHandle();
export default function PopoverDetachedTriggersSimpleDemo() {
return (
<React.Fragment>
<Popover.Trigger className="flex size-10 items-center justify-center rounded-md border border-gray-200 bg-gray-50 text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100 data-[popup-open]:bg-gray-100" handle={demoPopover}>
<BellIcon aria-label="Notifications" />
</Popover.Trigger>
<Popover.Root handle={demoPopover}>
<Popover.Portal>
<Popover.Positioner sideOffset={8}>
<Popover.Popup className="origin-[var(--transform-origin)] rounded-lg bg-[canvas] px-6 py-4 text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0 dark:shadow-none dark:-outline-offset-1 dark:outline-gray-300">
<Popover.Arrow className="data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180">
<ArrowSvg />
</Popover.Arrow>
<Popover.Title className="text-base font-medium">Notifications</Popover.Title>
<Popover.Description className="text-base text-gray-600">You are all caught up. Good job!</Popover.Description>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
</React.Fragment>
);
}
```
```tsx
/* icons-tw.tsx */
import * as React from "react";
export function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className="fill-[canvas]" />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className="fill-gray-200 dark:fill-none" />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className="dark:fill-gray-300" />
</svg>
);
}
export function BellIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="20" height="20" viewBox="0 0 16 16" {...props}>
<path d="M 8 1 C 7.453125 1 7 1.453125 7 2 L 7 3.140625 C 5.28125 3.589844 4 5.144531 4 7 L 4 10.984375 C 4 10.984375 3.984375 11.261719 3.851563 11.519531 C 3.71875 11.78125 3.558594 12 3 12 L 3 13 L 13 13 L 13 12 C 12.40625 12 12.253906 11.78125 12.128906 11.53125 C 12.003906 11.277344 12 11.003906 12 11.003906 L 12 7 C 12 5.144531 10.71875 3.589844 9 3.140625 L 9 2 C 9 1.453125 8.546875 1 8 1 Z M 8 13 C 7.449219 13 7 13.449219 7 14 C 7 14.550781 7.449219 15 8 15 C 8.550781 15 9 14.550781 9 14 C 9 13.449219 8.550781 13 8 13 Z M 8 4 C 9.664063 4 11 5.335938 11 7 L 11 10.996094 C 11 10.996094 10.988281 11.472656 11.234375 11.96875 C 11.238281 11.980469 11.246094 11.988281 11.25 12 L 4.726563 12 C 4.730469 11.992188 4.738281 11.984375 4.742188 11.980469 C 4.992188 11.488281 5 11.015625 5 11.015625 L 5 7 C 5 5.335938 6.335938 4 8 4 Z" />
</svg>
);
}
export function UserIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M18 20a6 6 0 0 0-12 0" />
<circle cx="12" cy="10" r="4" />
<circle cx="12" cy="12" r="10" />
</svg>
);
}
export function ListIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M3 5h.01" />
<path d="M3 12h.01" />
<path d="M3 19h.01" />
<path d="M8 5h13" />
<path d="M8 12h13" />
<path d="M8 19h13" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.IconButton {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
padding: 0;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Icon {
width: 1.25rem;
height: 1.25rem;
}
.Positioner {
width: var(--positioner-width);
height: var(--positioner-height);
max-width: var(--available-width);
}
.Popup {
box-sizing: border-box;
padding: 1rem 1.5rem;
border-radius: 0.5rem;
background-color: canvas;
color: var(--color-gray-900);
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
width: var(--popup-width, auto);
height: var(--popup-height, auto);
max-width: 500px;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.Title {
margin: 0;
font-size: 1rem;
line-height: 1.5rem;
font-weight: 500;
}
.Description {
margin: 0;
font-size: 1rem;
line-height: 1.5rem;
color: var(--color-gray-600);
}
.Container {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
}
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Popover } from "@base-ui/react/popover";
import styles from "./index.module.css";
const demoPopover = Popover.createHandle();
export default function PopoverDetachedTriggersSimpleDemo() {
return (
<React.Fragment>
<Popover.Trigger className={styles.IconButton} handle={demoPopover}>
<BellIcon aria-label="Notifications" className={styles.Icon} />
</Popover.Trigger>
<Popover.Root handle={demoPopover}>
<Popover.Portal>
<Popover.Positioner sideOffset={8}>
<Popover.Popup className={styles.Popup}>
<Popover.Arrow className={styles.Arrow}>
<ArrowSvg />
</Popover.Arrow>
<Popover.Title className={styles.Title}>Notifications</Popover.Title>
<Popover.Description className={styles.Description}>You are all caught up. Good job!</Popover.Description>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
</React.Fragment>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className={styles.ArrowFill} />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className={styles.ArrowOuterStroke} />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className={styles.ArrowInnerStroke} />
</svg>
);
}
function BellIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="20" height="20" viewBox="0 0 16 16" {...props}>
<path d="M 8 1 C 7.453125 1 7 1.453125 7 2 L 7 3.140625 C 5.28125 3.589844 4 5.144531 4 7 L 4 10.984375 C 4 10.984375 3.984375 11.261719 3.851563 11.519531 C 3.71875 11.78125 3.558594 12 3 12 L 3 13 L 13 13 L 13 12 C 12.40625 12 12.253906 11.78125 12.128906 11.53125 C 12.003906 11.277344 12 11.003906 12 11.003906 L 12 7 C 12 5.144531 10.71875 3.589844 9 3.140625 L 9 2 C 9 1.453125 8.546875 1 8 1 Z M 8 13 C 7.449219 13 7 13.449219 7 14 C 7 14.550781 7.449219 15 8 15 C 8.550781 15 9 14.550781 9 14 C 9 13.449219 8.550781 13 8 13 Z M 8 4 C 9.664063 4 11 5.335938 11 7 L 11 10.996094 C 11 10.996094 10.988281 11.472656 11.234375 11.96875 C 11.238281 11.980469 11.246094 11.988281 11.25 12 L 4.726563 12 C 4.730469 11.992188 4.738281 11.984375 4.742188 11.980469 C 4.992188 11.488281 5 11.015625 5 11.015625 L 5 7 C 5 5.335938 6.335938 4 8 4 Z" />
</svg>
);
}
```
### Multiple triggers
A single popover can be opened by multiple trigger elements.
You can achieve this by using the same `handle` for several detached triggers, or by placing multiple `<Popover.Trigger>` components inside a single `<Popover.Root>`.
```jsx title="Multiple triggers within the Root part"
<Popover.Root>
<Popover.Trigger>Trigger 1</Popover.Trigger>
<Popover.Trigger>Trigger 2</Popover.Trigger>
...
</Popover.Root>
```
```jsx title="Multiple detached triggers"
const demoPopover = Popover.createHandle();
<Popover.Trigger handle={demoPopover}>
Trigger 1
</Popover.Trigger>
<Popover.Trigger handle={demoPopover}>
Trigger 2
</Popover.Trigger>
<Popover.Root handle={demoPopover}>
...
</Popover.Root>
```
The popover can render different content depending on which trigger opened it.
This is achieved by passing a `payload` to the `<Popover.Trigger>` and using the function-as-a-child pattern in `<Popover.Root>`.
The payload can be strongly typed by providing a type argument to the `createHandle()` function:
```jsx title="Detached triggers with payload"
const demoPopover = Popover.createHandle<{ text: string }>();
<Popover.Trigger handle={demoPopover} payload={{ text: 'Trigger 1' }}>
Trigger 1
</Popover.Trigger>
<Popover.Trigger handle={demoPopover} payload={{ text: 'Trigger 2' }}>
Trigger 2
</Popover.Trigger>
<Popover.Root handle={demoPopover}>
{({ payload }) => (
<Popover.Portal>
<Popover.Positioner sideOffset={8}>
<Popover.Popup className={styles.Popup}>
<Popover.Arrow className={styles.Arrow}>
<ArrowSvg />
</Popover.Arrow>
<Popover.Title className={styles.Title}>Popover</Popover.Title>
{payload !== undefined && (
<Popover.Description className={styles.Description}>
This has been opened by {payload.text}
</Popover.Description>
)}
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
)}
</Popover.Root>
```
### Controlled mode with multiple triggers
You can control the popover's open state externally using the `open` and `onOpenChange` props on `<Popover.Root>`.
This allows you to manage the popover's visibility based on your application's state.
When using multiple triggers, you have to manage which trigger is active with the `triggerId` prop on `<Popover.Root>` and the `id` prop on each `<Popover.Trigger>`.
Note that there is no separate `onTriggerIdChange` prop.
Instead, the `onOpenChange` callback receives an additional argument, `eventDetails`, which contains the trigger element that initiated the state change.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Popover } from "@base-ui/react/popover";
import { ArrowSvg, BellIcon } from "./icons-tw";
const demoPopover = Popover.createHandle();
export default function PopoverDetachedTriggersSimpleDemo() {
const [open, setOpen] = React.useState(false);
const [triggerId, setTriggerId] = React.useState<string | null>(null);
const handleOpenChange = (isOpen: boolean, eventDetails: Popover.Root.ChangeEventDetails) => {
setOpen(isOpen);
setTriggerId(eventDetails.trigger?.id ?? null);
};
return (
<React.Fragment>
<div className="flex gap-2 flex-wrap justify-center">
<Popover.Trigger className="flex size-10 items-center justify-center rounded-md border border-gray-200 bg-gray-50 text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100 data-[popup-open]:bg-gray-100" handle={demoPopover} id="trigger-1">
<BellIcon aria-label="Notifications" />
</Popover.Trigger>
<Popover.Trigger className="flex size-10 items-center justify-center rounded-md border border-gray-200 bg-gray-50 text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100 data-[popup-open]:bg-gray-100" handle={demoPopover} id="trigger-2">
<BellIcon aria-label="Notifications" />
</Popover.Trigger>
<Popover.Trigger className="flex size-10 items-center justify-center rounded-md border border-gray-200 bg-gray-50 text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100 data-[popup-open]:bg-gray-100" handle={demoPopover} id="trigger-3">
<BellIcon aria-label="Notifications" />
</Popover.Trigger>
<button
type="button"
className="flex h-10 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3.5 text-base font-medium text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100"
onClick={() => {
setTriggerId("trigger-2");
setOpen(true);
}}
>
Open programmatically
</button>
</div>
<Popover.Root handle={demoPopover} open={open} onOpenChange={handleOpenChange} triggerId={triggerId}>
<Popover.Portal>
<Popover.Positioner className="h-(--positioner-height) w-(--positioner-width) max-w-(--available-width)" sideOffset={8}>
<Popover.Popup
className={`
h-(--popup-height,auto)
w-(--popup-width,auto) max-w-[500px]
origin-[var(--transform-origin)] rounded-lg bg-[canvas]
px-6 py-4
text-gray-900 shadow-lg
shadow-gray-200
outline-1
outline-gray-200
transition-[transform,scale,opacity]
data-[ending-style]:scale-90
data-[ending-style]:opacity-0
data-[starting-style]:scale-90
data-[starting-style]:opacity-0
dark:shadow-none
dark:-outline-offset-1
dark:outline-gray-300`}
>
<Popover.Arrow className="data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180">
<ArrowSvg />
</Popover.Arrow>
<Popover.Title className="text-base font-medium">Notifications</Popover.Title>
<Popover.Description className="text-base text-gray-600">You are all caught up. Good job!</Popover.Description>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
</React.Fragment>
);
}
```
```tsx
/* icons-tw.tsx */
import * as React from "react";
export function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className="fill-[canvas]" />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className="fill-gray-200 dark:fill-none" />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className="dark:fill-gray-300" />
</svg>
);
}
export function BellIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="20" height="20" viewBox="0 0 16 16" {...props}>
<path d="M 8 1 C 7.453125 1 7 1.453125 7 2 L 7 3.140625 C 5.28125 3.589844 4 5.144531 4 7 L 4 10.984375 C 4 10.984375 3.984375 11.261719 3.851563 11.519531 C 3.71875 11.78125 3.558594 12 3 12 L 3 13 L 13 13 L 13 12 C 12.40625 12 12.253906 11.78125 12.128906 11.53125 C 12.003906 11.277344 12 11.003906 12 11.003906 L 12 7 C 12 5.144531 10.71875 3.589844 9 3.140625 L 9 2 C 9 1.453125 8.546875 1 8 1 Z M 8 13 C 7.449219 13 7 13.449219 7 14 C 7 14.550781 7.449219 15 8 15 C 8.550781 15 9 14.550781 9 14 C 9 13.449219 8.550781 13 8 13 Z M 8 4 C 9.664063 4 11 5.335938 11 7 L 11 10.996094 C 11 10.996094 10.988281 11.472656 11.234375 11.96875 C 11.238281 11.980469 11.246094 11.988281 11.25 12 L 4.726563 12 C 4.730469 11.992188 4.738281 11.984375 4.742188 11.980469 C 4.992188 11.488281 5 11.015625 5 11.015625 L 5 7 C 5 5.335938 6.335938 4 8 4 Z" />
</svg>
);
}
export function UserIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M18 20a6 6 0 0 0-12 0" />
<circle cx="12" cy="10" r="4" />
<circle cx="12" cy="12" r="10" />
</svg>
);
}
export function ListIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M3 5h.01" />
<path d="M3 12h.01" />
<path d="M3 19h.01" />
<path d="M8 5h13" />
<path d="M8 12h13" />
<path d="M8 19h13" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.IconButton {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
padding: 0;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Icon {
width: 1.25rem;
height: 1.25rem;
}
.Positioner {
width: var(--positioner-width);
height: var(--positioner-height);
max-width: var(--available-width);
}
.Popup {
box-sizing: border-box;
padding: 1rem 1.5rem;
border-radius: 0.5rem;
background-color: canvas;
color: var(--color-gray-900);
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
width: var(--popup-width, auto);
height: var(--popup-height, auto);
max-width: 500px;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.Title {
margin: 0;
font-size: 1rem;
line-height: 1.5rem;
font-weight: 500;
}
.Description {
margin: 0;
font-size: 1rem;
line-height: 1.5rem;
color: var(--color-gray-600);
}
.Container {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
}
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Popover } from "@base-ui/react/popover";
import styles from "./index.module.css";
const demoPopover = Popover.createHandle();
export default function PopoverDetachedTriggersControlledDemo() {
const [open, setOpen] = React.useState(false);
const [triggerId, setTriggerId] = React.useState<string | null>(null);
const handleOpenChange = (isOpen: boolean, eventDetails: Popover.Root.ChangeEventDetails) => {
setOpen(isOpen);
setTriggerId(eventDetails.trigger?.id ?? null);
};
return (
<React.Fragment>
<div className={styles.Container}>
<Popover.Trigger className={styles.IconButton} handle={demoPopover} id="trigger-1">
<BellIcon aria-label="Notifications" className={styles.Icon} />
</Popover.Trigger>
<Popover.Trigger className={styles.IconButton} handle={demoPopover} id="trigger-2">
<BellIcon aria-label="Notifications" className={styles.Icon} />
</Popover.Trigger>
<Popover.Trigger className={styles.IconButton} handle={demoPopover} id="trigger-3">
<BellIcon aria-label="Notifications" className={styles.Icon} />
</Popover.Trigger>
<button
className={styles.Button}
type="button"
onClick={() => {
setTriggerId("trigger-2");
setOpen(true);
}}
>
Open programmatically
</button>
</div>
<Popover.Root handle={demoPopover} open={open} onOpenChange={handleOpenChange} triggerId={triggerId}>
<Popover.Portal>
<Popover.Positioner className={styles.Positioner} sideOffset={8}>
<Popover.Popup className={styles.Popup}>
<Popover.Arrow className={styles.Arrow}>
<ArrowSvg />
</Popover.Arrow>
<Popover.Title className={styles.Title}>Notifications</Popover.Title>
<Popover.Description className={styles.Description}>You are all caught up. Good job!</Popover.Description>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
</React.Fragment>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className={styles.ArrowFill} />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className={styles.ArrowOuterStroke} />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className={styles.ArrowInnerStroke} />
</svg>
);
}
function BellIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="20" height="20" viewBox="0 0 16 16" {...props}>
<path d="M 8 1 C 7.453125 1 7 1.453125 7 2 L 7 3.140625 C 5.28125 3.589844 4 5.144531 4 7 L 4 10.984375 C 4 10.984375 3.984375 11.261719 3.851563 11.519531 C 3.71875 11.78125 3.558594 12 3 12 L 3 13 L 13 13 L 13 12 C 12.40625 12 12.253906 11.78125 12.128906 11.53125 C 12.003906 11.277344 12 11.003906 12 11.003906 L 12 7 C 12 5.144531 10.71875 3.589844 9 3.140625 L 9 2 C 9 1.453125 8.546875 1 8 1 Z M 8 13 C 7.449219 13 7 13.449219 7 14 C 7 14.550781 7.449219 15 8 15 C 8.550781 15 9 14.550781 9 14 C 9 13.449219 8.550781 13 8 13 Z M 8 4 C 9.664063 4 11 5.335938 11 7 L 11 10.996094 C 11 10.996094 10.988281 11.472656 11.234375 11.96875 C 11.238281 11.980469 11.246094 11.988281 11.25 12 L 4.726563 12 C 4.730469 11.992188 4.738281 11.984375 4.742188 11.980469 C 4.992188 11.488281 5 11.015625 5 11.015625 L 5 7 C 5 5.335938 6.335938 4 8 4 Z" />
</svg>
);
}
```
### Animating the Popover
You can animate a popover as it moves between different trigger elements.
This includes animating its position, size, and content.
#### Position and Size
To animate the popover's position, apply CSS transitions to the `left`, `right`, `top`, and `bottom` properties of the **Positioner** part.
To animate its size, transition the `width` and `height` of the **Popup** part.
#### Content
The popover also supports content transitions.
This is useful when different triggers display different content within the same popover.
To enable content animations, wrap the content in the `<Popover.Viewport>` part.
This part provides features to create direction-aware animations.
It renders a `div` with a `data-activation-direction` attribute (`left`, `right`, `up`, or `down`) that indicates the new trigger's position relative to the previous one.
Inside the `<Popover.Viewport>`, the content is further wrapped in `div`s with data attributes to help with styling:
- `data-current`: The currently visible content when no transitions are present or the incoming content.
- `data-previous`: The outgoing content during a transition.
You can use these attributes to style the enter and exit animations.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Popover } from "@base-ui/react/popover";
import { Avatar } from "@base-ui/react/avatar";
import { ArrowSvg, BellIcon, ListIcon, UserIcon } from "./icons-tw";
const demoPopover = Popover.createHandle<React.ComponentType>();
export default function PopoverDetachedTriggersFullDemo() {
return (
<div className="flex gap-2">
<Popover.Trigger
className={`
box-border flex
size-10 items-center justify-center
rounded-md border border-gray-200
bg-gray-50
text-base font-bold text-gray-900
select-none
hover:bg-gray-100 focus-visible:outline-2
focus-visible:-outline-offset-1
focus-visible:outline-blue-600 active:bg-gray-100 data-popup-open:bg-gray-100`}
handle={demoPopover}
payload={NotificationsPanel}
>
<BellIcon aria-label="Notifications" className="size-5" />
</Popover.Trigger>
<Popover.Trigger
className={`
box-border flex
size-10 items-center justify-center
rounded-md border border-gray-200
bg-gray-50
text-base font-bold text-gray-900
select-none
hover:bg-gray-100 focus-visible:outline-2
focus-visible:-outline-offset-1
focus-visible:outline-blue-600 active:bg-gray-100 data-popup-open:bg-gray-100`}
handle={demoPopover}
payload={ActivityPanel}
>
<ListIcon aria-label="Activity" className="size-5" />
</Popover.Trigger>
<Popover.Trigger
className={`
box-border flex
size-10 items-center justify-center
rounded-md border border-gray-200
bg-gray-50
text-base font-bold text-gray-900
select-none
hover:bg-gray-100 focus-visible:outline-2
focus-visible:-outline-offset-1
focus-visible:outline-blue-600 active:bg-gray-100 data-popup-open:bg-gray-100`}
handle={demoPopover}
payload={ProfilePanel}
>
<UserIcon aria-label="Profile" className="size-5" />
</Popover.Trigger>
<Popover.Root handle={demoPopover}>
{({ payload: Payload }) => (
<Popover.Portal>
<Popover.Positioner
sideOffset={8}
className={`
h-(--positioner-height) w-(--positioner-width)
max-w-(--available-width)
transition-[top,left,right,bottom,transform]
duration-[0.35s]
ease-[cubic-bezier(0.22,1,0.36,1)]
data-instant:transition-none`}
>
<Popover.Popup
className={`
relative h-(--popup-height,auto) w-(--popup-width,auto)
max-w-[500px] origin-(--transform-origin)
rounded-lg bg-[canvas] text-gray-900
shadow-lg
shadow-gray-200
outline-1
outline-gray-200
transition-[width,height,opacity,scale]
duration-[0.35s]
ease-[cubic-bezier(0.22,1,0.36,1)]
data-ending-style:scale-90
data-ending-style:opacity-0 data-instant:transition-none
data-starting-style:scale-90
data-starting-style:opacity-0
dark:shadow-none
dark:-outline-offset-1
dark:outline-gray-300`}
>
<Popover.Arrow
className={`
flex
transition-[left] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)]
data-[side=bottom]:top-[-8px]
data-[side=left]:right-[-13px]
data-[side=left]:rotate-90
data-[side=right]:left-[-13px]
data-[side=right]:-rotate-90
data-[side=top]:bottom-[-8px]
data-[side=top]:rotate-180`}
>
<ArrowSvg />
</Popover.Arrow>
<Popover.Viewport
className={`
relative h-full w-full overflow-clip p-[1rem_1.5rem]
[&_[data-current]]:w-[calc(var(--popup-width)-3rem)]
[&_[data-current]]:translate-x-0
[&_[data-current]]:opacity-100
[&_[data-current]]:transition-[translate,opacity]
[&_[data-current]]:duration-[350ms,175ms]
[&_[data-current]]:ease-[cubic-bezier(0.22,1,0.36,1)]
data-[activation-direction~='left']:[&_[data-current][data-starting-style]]:-translate-x-1/2
data-[activation-direction~='left']:[&_[data-current][data-starting-style]]:opacity-0
data-[activation-direction~='right']:[&_[data-current][data-starting-style]]:translate-x-1/2
data-[activation-direction~='right']:[&_[data-current][data-starting-style]]:opacity-0
[&_[data-previous]]:w-[calc(var(--popup-width)-3rem)]
[&_[data-previous]]:translate-x-0
[&_[data-previous]]:opacity-100
[&_[data-previous]]:transition-[translate,opacity]
[&_[data-previous]]:duration-[350ms,175ms]
[&_[data-previous]]:ease-[cubic-bezier(0.22,1,0.36,1)]
data-[activation-direction~='left']:[&_[data-previous][data-ending-style]]:translate-x-1/2
data-[activation-direction~='left']:[&_[data-previous][data-ending-style]]:opacity-0
data-[activation-direction~='right']:[&_[data-previous][data-ending-style]]:-translate-x-1/2
data-[activation-direction~='right']:[&_[data-previous][data-ending-style]]:opacity-0`}
>
{Payload !== undefined && <Payload />}
</Popover.Viewport>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
)}
</Popover.Root>
</div>
);
}
function NotificationsPanel() {
return (
<React.Fragment>
<Popover.Title className="m-0 text-base font-medium">Notifications</Popover.Title>
<Popover.Description className="m-0 text-base text-gray-600">You are all caught up. Good job!</Popover.Description>
</React.Fragment>
);
}
function ProfilePanel() {
return (
<div className="-mx-2 grid grid-cols-[auto_auto] gap-x-4">
<Popover.Title className="col-start-2 col-end-3 row-start-1 row-end-2 m-0 text-base font-medium">Jason Eventon</Popover.Title>
<Avatar.Root className="col-start-1 col-end-2 row-start-1 row-end-3 inline-flex h-12 w-12 items-center justify-center overflow-hidden rounded-full bg-gray-100 align-middle text-base leading-none font-medium text-gray-900 select-none">
<Avatar.Image src="https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=128&h=128&dpr=2&q=80" width="48" height="48" className="h-full w-full object-cover" />
</Avatar.Root>
<span className="col-start-2 col-end-3 row-start-2 row-end-3 text-sm text-gray-600">Pro plan</span>
<div className="col-start-1 col-end-3 row-start-3 row-end-4 mt-2 flex flex-col gap-2 border-t border-gray-200 pt-2 text-sm">
<a href="#" className="text-gray-900 no-underline hover:underline">
Profile settings
</a>
<a href="#" className="text-gray-900 no-underline hover:underline">
Log out
</a>
</div>
</div>
);
}
function ActivityPanel() {
return (
<React.Fragment>
<Popover.Title className="m-0 text-base font-medium">Activity</Popover.Title>
<Popover.Description className="m-0 text-base text-gray-600">Nothing interesting happened recently.</Popover.Description>
</React.Fragment>
);
}
```
```tsx
/* icons-tw.tsx */
import * as React from "react";
export function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className="fill-[canvas]" />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className="fill-gray-200 dark:fill-none" />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className="dark:fill-gray-300" />
</svg>
);
}
export function BellIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="20" height="20" viewBox="0 0 16 16" {...props}>
<path d="M 8 1 C 7.453125 1 7 1.453125 7 2 L 7 3.140625 C 5.28125 3.589844 4 5.144531 4 7 L 4 10.984375 C 4 10.984375 3.984375 11.261719 3.851563 11.519531 C 3.71875 11.78125 3.558594 12 3 12 L 3 13 L 13 13 L 13 12 C 12.40625 12 12.253906 11.78125 12.128906 11.53125 C 12.003906 11.277344 12 11.003906 12 11.003906 L 12 7 C 12 5.144531 10.71875 3.589844 9 3.140625 L 9 2 C 9 1.453125 8.546875 1 8 1 Z M 8 13 C 7.449219 13 7 13.449219 7 14 C 7 14.550781 7.449219 15 8 15 C 8.550781 15 9 14.550781 9 14 C 9 13.449219 8.550781 13 8 13 Z M 8 4 C 9.664063 4 11 5.335938 11 7 L 11 10.996094 C 11 10.996094 10.988281 11.472656 11.234375 11.96875 C 11.238281 11.980469 11.246094 11.988281 11.25 12 L 4.726563 12 C 4.730469 11.992188 4.738281 11.984375 4.742188 11.980469 C 4.992188 11.488281 5 11.015625 5 11.015625 L 5 7 C 5 5.335938 6.335938 4 8 4 Z" />
</svg>
);
}
export function UserIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M18 20a6 6 0 0 0-12 0" />
<circle cx="12" cy="10" r="4" />
<circle cx="12" cy="12" r="10" />
</svg>
);
}
export function ListIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M3 5h.01" />
<path d="M3 12h.01" />
<path d="M3 19h.01" />
<path d="M8 5h13" />
<path d="M8 12h13" />
<path d="M8 19h13" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.IconButton {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
padding: 0;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
color: var(--color-gray-900);
user-select: none;
font-size: 1rem;
font-weight: bold;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Icon {
width: 1.25rem;
height: 1.25rem;
}
.Positioner {
--easing: cubic-bezier(0.22, 1, 0.36, 1);
--animation-duration: 0.35s;
width: var(--positioner-width);
height: var(--positioner-height);
max-width: var(--available-width);
transition-property: top, left, right, bottom, transform;
transition-timing-function: var(--easing);
transition-duration: var(--animation-duration);
/* Disable transitions when data-instant is set (used for the initial positioning of the popup) */
&[data-instant] {
transition: none;
}
}
.Popup {
position: relative;
background-color: canvas;
color: var(--color-gray-900);
border-radius: 0.5rem;
transform-origin: var(--transform-origin);
/* These are required to make the size animations work */
width: var(--popup-width, auto);
height: var(--popup-height, auto);
max-width: 500px;
/* width and height are essential for the resize animation; opacity and transform handle the enter/exit animation */
transition-property: width, height, opacity, transform;
transition-timing-function: var(--easing);
transition-duration: var(--animation-duration);
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
&[data-instant] {
transition: none;
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Viewport {
--viewport-inline-padding: 1.5rem;
/* Required to clip the overflowing content during the slide in/out animations */
position: relative;
overflow: clip;
width: 100%;
height: 100%;
padding: 1rem var(--viewport-inline-padding);
[data-previous],
[data-current] {
/* This freezes the width of the content while transitioning.
Width is set to the content area of the parent popup (--popup-width measures the border-box).
The 'previous` container receives the width of the previous content, while the `next` container
receives the width of the new content.
*/
width: calc(var(--popup-width) - 2 * var(--viewport-inline-padding));
transform: translateX(0);
opacity: 1;
transition:
transform var(--animation-duration) var(--easing),
opacity calc(var(--animation-duration) / 2) var(--easing);
}
&[data-activation-direction~="right"] [data-previous][data-ending-style] {
transform: translateX(-50%);
opacity: 0;
}
&[data-activation-direction~="right"] [data-current][data-starting-style] {
transform: translateX(50%);
opacity: 0;
}
&[data-activation-direction~="left"] [data-previous][data-ending-style] {
transform: translateX(50%);
opacity: 0;
}
&[data-activation-direction~="left"] [data-current][data-starting-style] {
transform: translateX(-50%);
opacity: 0;
}
}
.Arrow {
display: flex;
transition: left calc(var(--animation-duration)) var(--easing);
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.Title {
margin: 0;
font-size: 1rem;
line-height: 1.5rem;
font-weight: 500;
}
.Description {
margin: 0;
font-size: 1rem;
line-height: 1.5rem;
color: var(--color-gray-600);
}
.Container {
display: flex;
gap: 8px;
}
.ProfilePanel {
display: grid;
grid-template-columns: auto auto;
column-gap: 16px;
margin: 0 -8px;
.Title {
grid-column: 2;
grid-row: 1;
}
.Plan {
grid-column: 2;
grid-row: 2;
font-size: 0.875rem;
color: var(--color-gray-600);
}
.Avatar {
grid-column: 1;
grid-row: 1 / span 2;
display: inline-flex;
justify-content: center;
align-items: center;
vertical-align: middle;
border-radius: 100%;
user-select: none;
font-weight: 500;
color: var(--color-gray-900);
background-color: var(--color-gray-100);
font-size: 1rem;
line-height: 1;
overflow: hidden;
height: 3rem;
width: 3rem;
}
.AvatarImage {
object-fit: cover;
width: 100%;
height: 100%;
}
.ProfileActions {
grid-column: 1 / span 2;
grid-row: 3;
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px solid var(--color-gray-200);
font-size: 0.875rem;
a {
color: var(--color-gray-900);
text-decoration: none;
@media (hover: hover) {
&:hover {
text-decoration: underline;
}
}
}
}
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Popover } from "@base-ui/react/popover";
import { Avatar } from "@base-ui/react/avatar";
import styles from "./index.module.css";
const demoPopover = Popover.createHandle<React.ComponentType>();
export default function PopoverDetachedTriggersFullDemo() {
return (
<div className={styles.Container}>
<Popover.Trigger className={styles.IconButton} handle={demoPopover} payload={NotificationsPanel}>
<BellIcon aria-label="Notifications" className={styles.Icon} />
</Popover.Trigger>
<Popover.Trigger className={styles.IconButton} handle={demoPopover} payload={ActivityPanel}>
<ListIcon aria-label="Activity" className={styles.Icon} />
</Popover.Trigger>
<Popover.Trigger className={styles.IconButton} handle={demoPopover} payload={ProfilePanel}>
<UserIcon aria-label="Profile" className={styles.Icon} />
</Popover.Trigger>
<Popover.Root handle={demoPopover}>
{({ payload: Payload }) => (
<Popover.Portal>
<Popover.Positioner sideOffset={8} className={styles.Positioner}>
<Popover.Popup className={styles.Popup}>
<Popover.Arrow className={styles.Arrow}>
<ArrowSvg />
</Popover.Arrow>
<Popover.Viewport className={styles.Viewport}>{Payload !== undefined && <Payload />}</Popover.Viewport>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
)}
</Popover.Root>
</div>
);
}
function NotificationsPanel() {
return (
<React.Fragment>
<Popover.Title className={styles.Title}>Notifications</Popover.Title>
<Popover.Description className={styles.Description}>You are all caught up. Good job!</Popover.Description>
</React.Fragment>
);
}
function ProfilePanel() {
return (
<div className={styles.ProfilePanel}>
<Popover.Title className={styles.Title}>Jason Eventon</Popover.Title>
<Avatar.Root className={styles.Avatar}>
<Avatar.Image src="https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=128&h=128&dpr=2&q=80" width="48" height="48" className={styles.AvatarImage} />
</Avatar.Root>
<span className={styles.Plan}>Pro plan</span>
<div className={styles.ProfileActions}>
<a href="#">Profile settings</a>
<a href="#">Log out</a>
</div>
</div>
);
}
function ActivityPanel() {
return (
<React.Fragment>
<Popover.Title className={styles.Title}>Activity</Popover.Title>
<Popover.Description className={styles.Description}>Nothing interesting happened recently.</Popover.Description>
</React.Fragment>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className={styles.ArrowFill} />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className={styles.ArrowOuterStroke} />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className={styles.ArrowInnerStroke} />
</svg>
);
}
function BellIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="20" height="20" viewBox="0 0 16 16" {...props}>
<path d="M 8 1 C 7.453125 1 7 1.453125 7 2 L 7 3.140625 C 5.28125 3.589844 4 5.144531 4 7 L 4 10.984375 C 4 10.984375 3.984375 11.261719 3.851563 11.519531 C 3.71875 11.78125 3.558594 12 3 12 L 3 13 L 13 13 L 13 12 C 12.40625 12 12.253906 11.78125 12.128906 11.53125 C 12.003906 11.277344 12 11.003906 12 11.003906 L 12 7 C 12 5.144531 10.71875 3.589844 9 3.140625 L 9 2 C 9 1.453125 8.546875 1 8 1 Z M 8 13 C 7.449219 13 7 13.449219 7 14 C 7 14.550781 7.449219 15 8 15 C 8.550781 15 9 14.550781 9 14 C 9 13.449219 8.550781 13 8 13 Z M 8 4 C 9.664063 4 11 5.335938 11 7 L 11 10.996094 C 11 10.996094 10.988281 11.472656 11.234375 11.96875 C 11.238281 11.980469 11.246094 11.988281 11.25 12 L 4.726563 12 C 4.730469 11.992188 4.738281 11.984375 4.742188 11.980469 C 4.992188 11.488281 5 11.015625 5 11.015625 L 5 7 C 5 5.335938 6.335938 4 8 4 Z" />
</svg>
);
}
function UserIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M18 20a6 6 0 0 0-12 0" />
<circle cx="12" cy="10" r="4" />
<circle cx="12" cy="12" r="10" />
</svg>
);
}
function ListIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M3 5h.01" />
<path d="M3 12h.01" />
<path d="M3 19h.01" />
<path d="M8 5h13" />
<path d="M8 12h13" />
<path d="M8 19h13" />
</svg>
);
}
```
### Unmount control
Since `1.9.0`, automatic unmounting is decoupled from `actionsRef`. Popups used to unmount as soon as they closed, unless you passed an `actionsRef` and called `unmount()` yourself. Now the popup always unmounts on its own after the close animation, and you opt out explicitly.
To keep the popup mounted while an external animation library plays the exit transition, call `eventDetails.preventUnmountOnClose()` inside `onOpenChange`:
```tsx title="Keeping the popup mounted during an external exit animation"
import * as React from "react";
import { Popover } from "@base-ui/react/popover";
function ExamplePopover() {
const actionsRef = React.useRef<Popover.Root.Actions | null>(null);
return (
<Popover.Root
actionsRef={actionsRef}
onOpenChange={(open, eventDetails) => {
if (!open) {
// Opt out of the automatic unmount for this close.
eventDetails.preventUnmountOnClose();
}
}}
>
<Popover.Trigger>Open</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner>
<Popover.Popup>Content</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}
```
Then end the closing phase yourself once the external animation finishes:
```tsx
actionsRef.current?.unmount();
```
The same `eventDetails.preventUnmountOnClose()` pattern applies to Autocomplete, Combobox, Menu, Select, Dialog, Alert Dialog, Tooltip, and Preview Card.
Two separate things are involved, and they are easy to confuse:
- **Unmounting** is the end of the closing phase. `preventUnmountOnClose()` stops Base UI from ending that phase on its own, and `actionsRef.current.unmount()` ends it. The `unmount` action is only meaningful after `preventUnmountOnClose()` was called; without it the popup completes closing on its own.
- **Staying in the DOM** is decided by `keepMounted` on the portal. Setting `keepMounted` keeps the portal element in the DOM while the popup is hidden, which is independent of the closing phase.
```jsx title="keepMounted is separate from unmounting"
<Popover.Portal keepMounted>
<Popover.Positioner>
<Popover.Popup>Content</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
```
`eventDetails` also carries the other fields you already use, such as `reason`, `event`, and `trigger`. Cancelling the change (`eventDetails.cancel()`) prevents the open state change entirely and leaves the popup open.
## API reference
### Root
Groups all parts of the popover.
Doesn’t render its own HTML element.
**Root Props:**
| Prop | Type | Default | Description |
| :------------------------------------------------------ | :------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| defaultOpen | `boolean` | `false` | Whether the popover is initially open.To render a controlled popover, use the `open` prop instead. |
| open | `boolean` | - | Whether the popover is currently open. |
| onOpenChange | `((open: boolean, eventDetails: Popover.Root.ChangeEventDetails) => void)` | - | Event handler called when the popover is opened or closed. |
| actionsRef | `RefObject<Popover.Root.Actions \| null>` | - | A ref to imperative actions.\* `unmount`: Ends the closing phase of the popover after an externally controlled closing animation finishes.
Call `preventUnmountOnClose()` in `onOpenChange` first, otherwise the popover completes closing on its own.
Whether it leaves the DOM is decided by `keepMounted` on the portal. |
| \* `close`: Closes the popover imperatively when called. |
| defaultTriggerId | `string \| null` | - | ID of the trigger that the popover is associated with.
This is useful in conjunction with the `defaultOpen` prop to create an initially open popover. |
| handle | `Popover.Handle<Payload>` | - | A handle to associate the popover with a trigger.
If specified, allows external triggers to control the popover's open state. |
| modal | `boolean \| 'trap-focus'` | `false` | Determines if the popover enters a modal state when open.\* `true`: user interaction is limited to the popover: document page scroll is locked, and pointer interactions on outside elements are disabled. |
- `false`: user interaction with the rest of the document is allowed.
- `'trap-focus'`: focus is trapped inside the popover, but document page scroll is not locked and pointer interactions outside of it remain enabled. |
| onOpenChangeComplete | `((open: boolean) => void)` | - | Event handler called after any animations complete when the popover is opened or closed. |
| triggerId | `string \| null` | - | ID of the trigger that the popover is associated with.
This is useful in conjunction with the `open` prop to create a controlled popover.
There's no need to specify this prop when the popover is uncontrolled (i.e. when the `open` prop is not set). |
| children | `ReactNode \| PayloadChildRenderFunction<Payload>` | - | The content of the popover.
This can be a regular React node or a render function that receives the `payload` of the active trigger. |
### Trigger
A button that opens the popover.
Renders a `<button>` element.
**Trigger Props:**
| Prop | Type | Default | Description |
| :----------- | :----------------------------------------------------------------------------------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| handle | `Popover.Handle<Payload>` | - | A handle to associate the trigger with a popover. |
| nativeButton | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it
via the `render` prop.
Set to `false` if the rendered element is not a button (e.g. `<div>`). |
| payload | `Payload` | - | A payload to pass to the popover when it is opened. |
| openOnHover | `boolean` | `false` | Whether the popover should also open when the trigger is hovered. |
| delay | `number` | `300` | How long to wait before the popover may be opened on hover. Specified in milliseconds.Requires the `openOnHover` prop. |
| closeDelay | `number` | `0` | How long to wait before closing the popover that was opened on hover.
Specified in milliseconds.Requires the `openOnHover` prop. |
| id | `string` | - | ID of the trigger. In addition to being forwarded to the rendered element,
it is also used to specify the active trigger for the popover in controlled mode (with the Popover.Root `triggerId` prop). |
| className | `string \| ((state: Popover.Trigger.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Popover.Trigger.State) => CSSProperties \| undefined)` | - | Style applied to the element, or a function that
returns a style object based on the component’s state. |
| render | `ReactElement \| ((props: HTMLProps, state: Popover.Trigger.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Trigger Data Attributes:**
| Attribute | Type | Description |
| :-------------- | :--- | :---------------------------------------------- |
| data-popup-open | - | Present when the corresponding popover is open. |
| data-pressed | - | Present when the trigger is pressed. |
### Backdrop
An overlay displayed beneath the popover.
Renders a `<div>` element.
**Backdrop Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Popover.Backdrop.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Popover.Backdrop.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Popover.Backdrop.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Backdrop Data Attributes:**
| Attribute | Type | Description |
| :------------------ | :--- | :--------------------------------------- |
| data-open | - | Present when the popup is open. |
| data-closed | - | Present when the popup is closed. |
| data-starting-style | - | Present when the popup is animating in. |
| data-ending-style | - | Present when the popup is animating out. |
### Portal
A portal element that moves the popup to a different part of the DOM.
By default, the portal element is appended to `<body>`.
Renders a `<div>` element.
**Portal Props:**
| Prop | Type | Default | Description |
| :---------- | :---------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| container | `HTMLElement \| ShadowRoot \| RefObject<HTMLElement \| ShadowRoot \| null> \| null` | - | A parent element to render the portal element into. |
| className | `string \| ((state: Popover.Portal.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Popover.Portal.State) => CSSProperties \| undefined)` | - | - |
| keepMounted | `boolean` | `false` | Whether to keep the portal mounted in the DOM while the popup is hidden. |
| render | `ReactElement \| ((props: HTMLProps, state: Popover.Portal.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Positioner
Positions the popover against the trigger.
Renders a `<div>` element.
**Positioner Props:**
| Prop | Type | Default | Description |
| :-------------------- | :------------------------- | :--------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| disableAnchorTracking | `boolean` | `false` | Whether to disable the popup from tracking any layout shift of its positioning anchor. |
| align | `Align` | `'center'` | How to align the popup relative to the specified side. |
| alignOffset | `number \| OffsetFunction` | `0` | Additional offset along the alignment axis in pixels.
Also accepts a function that returns the offset to read the dimensions of the anchor
and positioner elements, along with its side and alignment.The function takes a `data` object parameter with the following properties:\* `data.anchor`: the dimensions of the anchor element with properties `width` and `height`. |
- `data.positioner`: the dimensions of the positioner element with properties `width` and `height`.
- `data.side`: which side of the anchor element the positioner is aligned against.
- `data.align`: how the positioner is aligned relative to the specified side. |
| side | `Side` | `'bottom'` | Which side of the anchor element to align the popup against.
May automatically change to avoid collisions. |
| sideOffset | `number \| OffsetFunction` | `0` | Distance between the anchor and the popup in pixels.
Also accepts a function that returns the distance to read the dimensions of the anchor
and positioner elements, along with its side and alignment.The function takes a `data` object parameter with the following properties:\* `data.anchor`: the dimensions of the anchor element with properties `width` and `height`.
- `data.positioner`: the dimensions of the positioner element with properties `width` and `height`.
- `data.side`: which side of the anchor element the positioner is aligned against.
- `data.align`: how the positioner is aligned relative to the specified side. |
| arrowPadding | `number` | `5` | Minimum distance to maintain between the arrow and the edges of the popup.Use it to prevent the arrow element from hanging out of the rounded corners of a popup. |
| anchor | `Element \| RefObject<Element \| null> \| VirtualElement \| (() => Element \| VirtualElement \| null) \| null` | - | An element to position the popup against.
By default, the popup will be positioned against the trigger. |
| collisionAvoidance | `CollisionAvoidance` | - | Determines how to handle collisions when positioning the popup. |
| collisionBoundary | `Boundary` | `'clipping-ancestors'` | An element or a rectangle that delimits the area that the popup is confined to. |
| collisionPadding | `Padding` | `5` | Additional space to maintain from the edge of the collision boundary. |
| sticky | `boolean` | `false` | Whether to maintain the popup in the viewport after
the anchor element was scrolled out of view. |
| positionMethod | `'fixed' \| 'absolute'` | `'absolute'` | Determines which CSS `position` property to use. |
| className | `string \| ((state: Popover.Positioner.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Popover.Positioner.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Popover.Positioner.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Positioner Data Attributes:**
| Attribute | Type | Description |
| :----------------- | :------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| data-open | - | Present when the popup is open. |
| data-closed | - | Present when the popup is closed. |
| data-anchor-hidden | - | Present when the anchor is hidden. |
| data-align | `'start' \| 'center' \| 'end'` | Indicates how the popup is aligned relative to specified side. |
| data-side | `'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the popup is positioned relative to the trigger. |
**Positioner CSS Variables:**
| Variable | Type | Default | Description |
| :------------------ | :------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------- |
| --anchor-height | `number` | - | The anchor's height. |
| --anchor-width | `number` | - | The anchor's width. |
| --available-height | `number` | - | The available height between the trigger and the edge of the viewport. |
| --available-width | `number` | - | The available width between the trigger and the edge of the viewport. |
| --positioner-height | - | - | The height of the popover's positioner.
It is important to set `height` to this value when using CSS to animate size changes. |
| --positioner-width | - | - | The width of the popover's positioner.
It is important to set `width` to this value when using CSS to animate size changes. |
| --transform-origin | `string` | - | The coordinates that this element is anchored to. Used for animations and transitions. |
### Popup
A container for the popover contents.
Renders a `<div>` element.
**Popup Props:**
| Prop | Type | Default | Description |
| :----------- | :--------------------------------------------------------------------------------------------------------------------- | :------ | :---------------------------------------------------------------------------------------- |
| initialFocus | `boolean \| RefObject<HTMLElement \| null> \| ((openType: InteractionType) => boolean \| void \| HTMLElement \| null)` | - | Determines the element to focus when the popover is opened.\* `false`: Do not move focus. |
- `true`: Move focus based on the default behavior (first tabbable element or popup).
- `RefObject`: Move focus to the ref element.
- `function`: Called with the interaction type (`mouse`, `touch`, `pen`, or `keyboard`).
Return an element to focus, `true` to use the default behavior, or `false`/`undefined` to do nothing. |
| finalFocus | `boolean \| RefObject<HTMLElement \| null> \| ((closeType: InteractionType) => boolean \| void \| HTMLElement \| null)` | - | Determines the element to focus when the popover is closed.\* `false`: Do not move focus.
- `true`: Move focus based on the default behavior (trigger or previously focused element).
- `RefObject`: Move focus to the ref element.
- `function`: Called with the interaction type (`mouse`, `touch`, `pen`, or `keyboard`).
Return an element to focus, `true` to use the default behavior, or `false`/`undefined` to do nothing. |
| className | `string \| ((state: Popover.Popup.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Popover.Popup.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Popover.Popup.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Popup Data Attributes:**
| Attribute | Type | Description |
| :------------------ | :------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| data-open | - | Present when the popup is open. |
| data-closed | - | Present when the popup is closed. |
| data-align | `'start' \| 'center' \| 'end'` | Indicates how the popup is aligned relative to specified side. |
| data-instant | `'click' \| 'dismiss'` | Present if animations should be instant. |
| data-side | `'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the popup is positioned relative to the trigger. |
| data-starting-style | - | Present when the popup is animating in. |
| data-ending-style | - | Present when the popup is animating out. |
**Popup CSS Variables:**
| Variable | Type | Default | Description |
| :------------- | :--- | :------ | :----------------------- |
| --popup-height | - | - | The height of the popup. |
| --popup-width | - | - | The width of the popup. |
### Arrow
Displays an element positioned against the popover anchor.
Renders a `<div>` element.
**Arrow Props:**
| Prop | Type | Default | Description |
| :-------- | :--------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Popover.Arrow.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Popover.Arrow.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Popover.Arrow.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Arrow Data Attributes:**
| Attribute | Type | Description |
| :-------------- | :------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| data-open | - | Present when the popup is open. |
| data-closed | - | Present when the popup is closed. |
| data-uncentered | - | Present when the popover arrow is uncentered. |
| data-align | `'start' \| 'center' \| 'end'` | Indicates how the popup is aligned relative to specified side. |
| data-side | `'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the popup is positioned relative to the trigger. |
### Title
A heading that labels the popover.
Renders an `<h2>` element.
**Title Props:**
| Prop | Type | Default | Description |
| :-------- | :--------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Popover.Title.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Popover.Title.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Popover.Title.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Description
A paragraph with additional information about the popover.
Renders a `<p>` element.
**Description Props:**
| Prop | Type | Default | Description |
| :-------- | :--------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Popover.Description.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Popover.Description.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Popover.Description.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Close
A button that closes the popover.
Renders a `<button>` element.
**Close Props:**
| Prop | Type | Default | Description |
| :----------- | :--------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| nativeButton | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it
via the `render` prop.
Set to `false` if the rendered element is not a button (e.g. `<div>`). |
| className | `string \| ((state: Popover.Close.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Popover.Close.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Popover.Close.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Viewport
A viewport for displaying content transitions.
This component is only required if one popup can be opened by multiple triggers, its content change based on the trigger
and switching between them is animated.
Renders a `<div>` element.
**Viewport Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| children | `ReactNode` | - | The content to render inside the transition container. |
| className | `string \| ((state: Popover.Viewport.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Popover.Viewport.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Popover.Viewport.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Viewport Data Attributes:**
| Attribute | Type | Description |
| :------------------------ | :----------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| data-activation-direction | `` `${'left' \| 'right'} {'top' \| 'bottom'}` `` | Indicates the direction from which the popup was activated.
This can be used to create directional animations based on how the popup was triggered.
Contains space-separated values for both horizontal and vertical axes. |
| data-current | - | Applied to the direct child of the viewport when no transitions are present or the new content when it's entering. |
| data-previous | - | Applied to the direct child of the viewport that contains the exiting content when transitions are present. |
| data-transitioning | - | Indicates that the viewport is currently transitioning between old and new content. |
**Viewport CSS Variables:**
| Variable | Type | Default | Description |
| :------------- | :--- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| --popup-height | - | - | The height of the parent popup.
This variable is placed on the 'previous' container and stores the height of the popup when the previous content was rendered.
It can be used to freeze the dimensions of the popup when animating between different content. |
| --popup-width | - | - | The width of the parent popup.
This variable is placed on the 'previous' container and stores the width of the popup when the previous content was rendered.
It can be used to freeze the dimensions of the popup when animating between different content. |
references/preview-card.md
---
title: Preview Card
subtitle: A popup that appears when a link is hovered, showing a preview for sighted users.
description: A high-quality, unstyled React preview card component that appears when a link is hovered, showing a preview for sighted users.
---
# Preview Card
<Meta name="description" content="A high-quality, unstyled React preview card component that appears when a link is hovered, showing a preview for sighted users." />
## v1.9.0 notes
- Automatic unmounting is decoupled from `actionsRef`. Call `eventDetails.preventUnmountOnClose()` in `onOpenChange` to opt out, then end the closing phase with `actionsRef.current.unmount()`. See `popover.md`.
- A viewport transition no longer starts when a retained popup reopens.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import * as React from 'react';
import { PreviewCard } from '@base-ui/react/preview-card';
export default function ExamplePreviewCard() {
return (
<PreviewCard.Root>
<p className="m-0 text-base leading-6 text-gray-900 text-balance">
The principles of good{' '}
<PreviewCard.Trigger
className="text-blue-800 no-underline decoration-blue-800/60 decoration-1 underline-offset-2 outline-0 hover:underline data-[popup-open]:underline focus-visible:rounded-[2px] focus-visible:no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-800"
href="https://en.wikipedia.org/wiki/Typography"
>
typography
</PreviewCard.Trigger>{' '}
remain in the digital age.
</p>
<PreviewCard.Portal>
<PreviewCard.Positioner sideOffset={8}>
<PreviewCard.Popup className="box-border w-[var(--popup-width,auto)] h-[var(--popup-height,auto)] rounded-lg bg-[canvas] origin-[var(--transform-origin)] transition-[scale,opacity] duration-150 data-[starting-style]:scale-90 data-[starting-style]:opacity-0 data-[ending-style]:scale-90 data-[ending-style]:opacity-0 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 dark:shadow-none dark:outline-gray-300 dark:-outline-offset-1">
<PreviewCard.Arrow className="flex data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180">
<ArrowSvg />
</PreviewCard.Arrow>
<div className="w-min flex flex-col gap-2 p-2 box-border">
<img
width="224"
height="150"
className="block rounded-sm max-w-none"
src="https://images.unsplash.com/photo-1619615391095-dfa29e1672ef?q=80&w=448&h=300"
alt="Station Hofplein signage in Rotterdam, Netherlands"
/>
<p className="m-0 text-sm leading-5 text-gray-900 text-pretty">
<strong>Typography</strong> is the art and science of arranging type to make written
language clear, visually appealing, and effective in communication.
</p>
</div>
</PreviewCard.Popup>
</PreviewCard.Positioner>
</PreviewCard.Portal>
</PreviewCard.Root>
);
}
function ArrowSvg(props: React.ComponentProps<'svg'>) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
className="fill-[canvas]"
/>
<path
d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
className="fill-gray-200 dark:fill-none"
/>
<path
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
className="dark:fill-gray-300"
/>
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```tsx
/* index.tsx */
import * as React from 'react';
import { PreviewCard } from '@base-ui/react/preview-card';
import styles from './demos.module.css';
export default function ExamplePreviewCard() {
return (
<PreviewCard.Root>
<p className={styles.Paragraph}>
The principles of good{' '}
<PreviewCard.Trigger
className={styles.Link}
href="https://en.wikipedia.org/wiki/Typography"
>
typography
</PreviewCard.Trigger>{' '}
remain in the digital age.
</p>
<PreviewCard.Portal>
<PreviewCard.Positioner sideOffset={8}>
<PreviewCard.Popup className={styles.Popup}>
<PreviewCard.Arrow className={styles.Arrow}>
<ArrowSvg />
</PreviewCard.Arrow>
<div className={styles.PopupContent}>
<img
width="224"
height="150"
className={styles.Image}
src="https://images.unsplash.com/photo-1619615391095-dfa29e1672ef?q=80&w=448&h=300"
alt="Station Hofplein signage in Rotterdam, Netherlands"
/>
<p className={styles.Summary}>
<strong>Typography</strong> is the art and science of arranging type to make written
language clear, visually appealing, and effective in communication.
</p>
</div>
</PreviewCard.Popup>
</PreviewCard.Positioner>
</PreviewCard.Portal>
</PreviewCard.Root>
);
}
function ArrowSvg(props: React.ComponentProps<'svg'>) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
className={styles.ArrowFill}
/>
<path
d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
className={styles.ArrowOuterStroke}
/>
<path
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
className={styles.ArrowInnerStroke}
/>
</svg>
);
}
```
```css
/* demos.module.css */
.Positioner {
height: var(--positioner-height);
width: var(--positioner-width);
max-width: var(--available-width);
}
.Popup {
box-sizing: border-box;
width: var(--popup-width, auto);
height: var(--popup-height, auto);
border-radius: 0.5rem;
background-color: canvas;
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
&[data-side='top'] {
bottom: -8px;
rotate: 180deg;
}
&[data-side='bottom'] {
top: -8px;
rotate: 0deg;
}
&[data-side='left'] {
right: -13px;
rotate: 90deg;
}
&[data-side='right'] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.PopupContent {
width: min-content;
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.5rem;
box-sizing: border-box;
}
.Image {
display: block;
border-radius: 0.25rem;
max-width: none;
}
.Summary {
margin: 0;
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--color-gray-900);
text-wrap: pretty;
}
.Container {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: center;
align-items: baseline;
}
.Paragraph {
margin: 0;
font-size: 1rem;
line-height: 1.5rem;
color: var(--color-gray-900);
text-wrap: balance;
}
.Link {
outline: 0;
color: var(--color-blue);
text-decoration-line: none;
text-decoration-thickness: 1px;
text-decoration-color: color-mix(in oklab, var(--color-blue), transparent 40%);
text-underline-offset: 2px;
@media (hover: hover) {
&:hover {
text-decoration-line: underline;
}
}
&[data-popup-open] {
text-decoration-line: underline;
}
&:focus-visible {
border-radius: 0.125rem;
outline: 2px solid var(--color-blue);
text-decoration-line: none;
}
}
.LinkGroup {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
align-items: baseline;
}
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
```
## Anatomy
Import the component and assemble its parts:
```jsx title="Anatomy"
import { PreviewCard } from '@base-ui/react/preview-card';
<PreviewCard.Root>
<PreviewCard.Trigger />
<PreviewCard.Portal>
<PreviewCard.Backdrop />
<PreviewCard.Positioner>
<PreviewCard.Popup>
<PreviewCard.Arrow />
</PreviewCard.Popup>
</PreviewCard.Positioner>
</PreviewCard.Portal>
</PreviewCard.Root>;
```
## Examples
### Detached triggers
A preview card can be controlled by a trigger located either inside or outside the `<PreviewCard.Root>` component.
For simple, one-off interactions, place the `<PreviewCard.Trigger>` inside `<PreviewCard.Root>`, as shown in the example at the top of this page.
However, if defining the preview card's content next to its trigger is not practical, you can use a detached trigger.
This involves placing the `<PreviewCard.Trigger>` outside of `<PreviewCard.Root>` and linking them with a `handle` created by the `PreviewCard.createHandle()` function.
```jsx title="Detached triggers" {3,5} "handle={demoPreviewCard}"
const demoPreviewCard = PreviewCard.createHandle();
<PreviewCard.Trigger handle={demoPreviewCard} href="#">
Link
</PreviewCard.Trigger>
<PreviewCard.Root handle={demoPreviewCard}>
...
</PreviewCard.Root>
```
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
'use client';
import * as React from 'react';
import { PreviewCard } from '@base-ui/react/preview-card';
const demoPreviewCard = PreviewCard.createHandle();
export default function PreviewCardDetachedTriggersSimpleDemo() {
return (
<div>
<p className="m-0 text-base leading-6 text-gray-900 text-balance">
The principles of good{' '}
<PreviewCard.Trigger
className="text-blue-800 no-underline decoration-blue-800/60 decoration-1 underline-offset-2 outline-0 hover:underline data-[popup-open]:underline focus-visible:rounded-[2px] focus-visible:no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-800"
handle={demoPreviewCard}
href="https://en.wikipedia.org/wiki/Typography"
>
typography
</PreviewCard.Trigger>{' '}
remain in the digital age.
</p>
<PreviewCard.Root handle={demoPreviewCard}>
<PreviewCard.Portal>
<PreviewCard.Positioner sideOffset={8}>
<PreviewCard.Popup className="box-border w-[var(--popup-width,auto)] h-[var(--popup-height,auto)] rounded-lg bg-[canvas] origin-[var(--transform-origin)] transition-[scale,opacity] duration-150 data-[starting-style]:scale-90 data-[starting-style]:opacity-0 data-[ending-style]:scale-90 data-[ending-style]:opacity-0 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 dark:shadow-none dark:outline-gray-300 dark:-outline-offset-1">
<PreviewCard.Arrow className="flex data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180">
<ArrowSvg />
</PreviewCard.Arrow>
<div className="w-min flex flex-col gap-2 p-2 box-border">
<img
width="224"
height="150"
className="block rounded-sm max-w-none"
src="https://images.unsplash.com/photo-1619615391095-dfa29e1672ef?q=80&w=448&h=300"
alt="Station Hofplein signage in Rotterdam, Netherlands"
/>
<p className="m-0 text-sm leading-5 text-gray-900 text-pretty">
<strong>Typography</strong> is the art and science of arranging type to make
written language clear, visually appealing, and effective in communication.
</p>
</div>
</PreviewCard.Popup>
</PreviewCard.Positioner>
</PreviewCard.Portal>
</PreviewCard.Root>
</div>
);
}
function ArrowSvg(props: React.ComponentProps<'svg'>) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
className="fill-[canvas]"
/>
<path
d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
className="fill-gray-200 dark:fill-none"
/>
<path
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
className="dark:fill-gray-300"
/>
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```tsx
/* index.tsx */
'use client';
import * as React from 'react';
import { PreviewCard } from '@base-ui/react/preview-card';
import styles from './demos.module.css';
const demoPreviewCard = PreviewCard.createHandle();
export default function PreviewCardDetachedTriggersSimpleDemo() {
return (
<div>
<p className={styles.Paragraph}>
The principles of good{' '}
<PreviewCard.Trigger
className={styles.Link}
handle={demoPreviewCard}
href="https://en.wikipedia.org/wiki/Typography"
>
typography
</PreviewCard.Trigger>{' '}
remain in the digital age.
</p>
<PreviewCard.Root handle={demoPreviewCard}>
<PreviewCard.Portal>
<PreviewCard.Positioner sideOffset={8}>
<PreviewCard.Popup className={styles.Popup}>
<PreviewCard.Arrow className={styles.Arrow}>
<ArrowSvg />
</PreviewCard.Arrow>
<div className={styles.PopupContent}>
<img
width="224"
height="150"
className={styles.Image}
src="https://images.unsplash.com/photo-1619615391095-dfa29e1672ef?q=80&w=448&h=300"
alt="Station Hofplein signage in Rotterdam, Netherlands"
/>
<p className={styles.Summary}>
<strong>Typography</strong> is the art and science of arranging type to make
written language clear, visually appealing, and effective in communication.
</p>
</div>
</PreviewCard.Popup>
</PreviewCard.Positioner>
</PreviewCard.Portal>
</PreviewCard.Root>
</div>
);
}
function ArrowSvg(props: React.ComponentProps<'svg'>) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
className={styles.ArrowFill}
/>
<path
d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
className={styles.ArrowOuterStroke}
/>
<path
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
className={styles.ArrowInnerStroke}
/>
</svg>
);
}
```
```css
/* demos.module.css */
.Positioner {
height: var(--positioner-height);
width: var(--positioner-width);
max-width: var(--available-width);
}
.Popup {
box-sizing: border-box;
width: var(--popup-width, auto);
height: var(--popup-height, auto);
border-radius: 0.5rem;
background-color: canvas;
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
&[data-side='top'] {
bottom: -8px;
rotate: 180deg;
}
&[data-side='bottom'] {
top: -8px;
rotate: 0deg;
}
&[data-side='left'] {
right: -13px;
rotate: 90deg;
}
&[data-side='right'] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.PopupContent {
width: min-content;
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.5rem;
box-sizing: border-box;
}
.Image {
display: block;
border-radius: 0.25rem;
max-width: none;
}
.Summary {
margin: 0;
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--color-gray-900);
text-wrap: pretty;
}
.Container {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: center;
align-items: baseline;
}
.Paragraph {
margin: 0;
font-size: 1rem;
line-height: 1.5rem;
color: var(--color-gray-900);
text-wrap: balance;
}
.Link {
outline: 0;
color: var(--color-blue);
text-decoration-line: none;
text-decoration-thickness: 1px;
text-decoration-color: color-mix(in oklab, var(--color-blue), transparent 40%);
text-underline-offset: 2px;
@media (hover: hover) {
&:hover {
text-decoration-line: underline;
}
}
&[data-popup-open] {
text-decoration-line: underline;
}
&:focus-visible {
border-radius: 0.125rem;
outline: 2px solid var(--color-blue);
text-decoration-line: none;
}
}
.LinkGroup {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
align-items: baseline;
}
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
```
### Multiple triggers
A single preview card can be opened by multiple trigger elements.
You can achieve this by using the same `handle` for several detached triggers, or by placing multiple `<PreviewCard.Trigger>` components inside a single `<PreviewCard.Root>`.
```jsx title="Multiple triggers within the Root part"
<PreviewCard.Root>
<PreviewCard.Trigger href="#">Trigger 1</PreviewCard.Trigger>
<PreviewCard.Trigger href="#">Trigger 2</PreviewCard.Trigger>
...
</PreviewCard.Root>
```
```jsx title="Multiple detached triggers"
const demoPreviewCard = PreviewCard.createHandle();
<PreviewCard.Trigger handle={demoPreviewCard} href="#">
Trigger 1
</PreviewCard.Trigger>
<PreviewCard.Trigger handle={demoPreviewCard} href="#">
Trigger 2
</PreviewCard.Trigger>
<PreviewCard.Root handle={demoPreviewCard}>
...
</PreviewCard.Root>
```
The preview card can render different content depending on which trigger opened it.
This is achieved by passing a `payload` to the `<PreviewCard.Trigger>` and using the function-as-a-child pattern in `<PreviewCard.Root>`.
The payload can be strongly typed by providing a type argument to the `createHandle()` function:
```jsx title="Detached triggers with payload" {1,3,7} "payload"
const demoPreviewCard = PreviewCard.createHandle<{ title: string }>();
<PreviewCard.Trigger handle={demoPreviewCard} payload={{ title: 'Trigger 1' }} href="#">
Trigger 1
</PreviewCard.Trigger>
<PreviewCard.Trigger handle={demoPreviewCard} payload={{ title: 'Trigger 2' }} href="#">
Trigger 2
</PreviewCard.Trigger>
<PreviewCard.Root handle={demoPreviewCard}>
{({ payload }) => (
<PreviewCard.Portal>
<PreviewCard.Positioner sideOffset={8}>
<PreviewCard.Popup>
{payload !== undefined && (
<span>
Preview card opened by {payload.title}
</span>
)}
</PreviewCard.Popup>
</PreviewCard.Positioner>
</PreviewCard.Portal>
)}
</PreviewCard.Root>
```
### Controlled mode with multiple triggers
You can control the preview card's open state externally using the `open` and `onOpenChange` props on `<PreviewCard.Root>`.
This allows you to manage the preview card's visibility based on your application's state.
When using multiple triggers, you have to manage which trigger is active with the `triggerId` prop on `<PreviewCard.Root>` and the `id` prop on each `<PreviewCard.Trigger>`.
Note that there is no separate `onTriggerIdChange` prop.
Instead, the `onOpenChange` callback receives an additional argument, `eventDetails`, which contains the trigger element that initiated the state change.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
'use client';
import * as React from 'react';
import { PreviewCard } from '@base-ui/react/preview-card';
const demoPreviewCard = PreviewCard.createHandle<React.ReactElement>();
const cardContents = {
typography: (
<div className="w-min flex flex-col gap-2 p-2 box-border">
<img
width="224"
height="150"
className="block rounded-sm max-w-none"
src="https://images.unsplash.com/photo-1619615391095-dfa29e1672ef?q=80&w=448&h=300"
alt="Station Hofplein signage in Rotterdam, Netherlands"
/>
<p className="m-0 text-sm leading-5 text-gray-900 text-pretty">
<strong>Typography</strong> is the art and science of arranging type.
</p>
</div>
),
design: (
<div className="w-min flex flex-col gap-2 p-2 box-border">
<img
width="241"
height="240"
className="block rounded-sm max-w-none"
src="https://upload.wikimedia.org/wikipedia/commons/thumb/b/b7/Braun_ABW30_%28schwarz%29.jpg/250px-Braun_ABW30_%28schwarz%29.jpg"
alt="Braun ABW30"
/>
<p className="m-0 text-sm leading-5 text-gray-900 text-pretty">
A <strong>design</strong> is the concept or proposal for an object, process, or system.
</p>
</div>
),
art: (
<div className="w-min flex flex-col gap-2 p-2 box-border">
<img
width="206"
height="240"
className="block rounded-sm max-w-none"
src="https://upload.wikimedia.org/wikipedia/commons/thumb/5/5f/MonaLisa_sfumato.jpeg/250px-MonaLisa_sfumato.jpeg"
alt="Mona Lisa"
/>
<p className="m-0 text-sm leading-5 text-gray-900 text-pretty">
<strong>Art</strong> is a diverse range of cultural activity centered around works utilizing
creative or imaginative talents, which are expected to evoke a worthwhile experience,
generally through an expression of emotional power, conceptual ideas, technical proficiency,
or beauty.
</p>
</div>
),
};
export default function PreviewCardDetachedTriggersControlledDemo() {
const [open, setOpen] = React.useState(false);
const [triggerId, setTriggerId] = React.useState<string | null>(null);
const handleOpenChange = (isOpen: boolean, eventDetails: PreviewCard.Root.ChangeEventDetails) => {
setOpen(isOpen);
setTriggerId(eventDetails.trigger?.id ?? null);
};
return (
<div>
<div className="flex gap-2 flex-wrap justify-center items-baseline">
<p className="m-0 text-base leading-6 text-gray-900 text-balance">
Discover{' '}
<PreviewCard.Trigger
className="text-blue-800 no-underline decoration-blue-800/60 decoration-1 underline-offset-2 outline-0 hover:underline data-[popup-open]:underline focus-visible:rounded-[2px] focus-visible:no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-800"
handle={demoPreviewCard}
href="https://en.wikipedia.org/wiki/Typography"
id="trigger-1"
payload={cardContents.typography}
>
typography
</PreviewCard.Trigger>
,{' '}
<PreviewCard.Trigger
className="text-blue-800 no-underline decoration-blue-800/60 decoration-1 underline-offset-2 outline-0 hover:underline data-[popup-open]:underline focus-visible:rounded-[2px] focus-visible:no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-800"
handle={demoPreviewCard}
href="https://en.wikipedia.org/wiki/Industrial_design"
id="trigger-2"
payload={cardContents.design}
>
design
</PreviewCard.Trigger>
, or{' '}
<PreviewCard.Trigger
className="text-blue-800 no-underline decoration-blue-800/60 decoration-1 underline-offset-2 outline-0 hover:underline data-[popup-open]:underline focus-visible:rounded-[2px] focus-visible:no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-800"
handle={demoPreviewCard}
href="https://en.wikipedia.org/wiki/Art"
id="trigger-3"
payload={cardContents.art}
>
art
</PreviewCard.Trigger>
.
</p>
<button
type="button"
className="box-border flex items-center justify-center h-10 px-3.5 m-0 outline-0 border border-gray-200 rounded-md bg-gray-50 font-inherit text-base font-medium leading-6 text-gray-900 select-none hover:bg-gray-100 active:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-800 focus-visible:-outline-offset-1"
onClick={() => {
setTriggerId('trigger-2');
setOpen(true);
}}
>
Open programmatically
</button>
</div>
<PreviewCard.Root
handle={demoPreviewCard}
open={open}
onOpenChange={handleOpenChange}
triggerId={triggerId}
>
{({ payload }) => (
<PreviewCard.Portal>
<PreviewCard.Positioner
sideOffset={8}
className="h-[var(--positioner-height)] w-[var(--positioner-width)] max-w-[var(--available-width)]"
>
<PreviewCard.Popup className="box-border w-[var(--popup-width,auto)] h-[var(--popup-height,auto)] rounded-lg bg-[canvas] origin-[var(--transform-origin)] transition-[scale,opacity] duration-150 data-[starting-style]:scale-90 data-[starting-style]:opacity-0 data-[ending-style]:scale-90 data-[ending-style]:opacity-0 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 dark:shadow-none dark:outline-gray-300 dark:-outline-offset-1">
<PreviewCard.Arrow className="flex data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180">
<ArrowSvg />
</PreviewCard.Arrow>
{payload}
</PreviewCard.Popup>
</PreviewCard.Positioner>
</PreviewCard.Portal>
)}
</PreviewCard.Root>
</div>
);
}
function ArrowSvg(props: React.ComponentProps<'svg'>) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
className="fill-[canvas]"
/>
<path
d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
className="fill-gray-200 dark:fill-none"
/>
<path
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
className="dark:fill-gray-300"
/>
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```tsx
/* index.tsx */
'use client';
import * as React from 'react';
import { PreviewCard } from '@base-ui/react/preview-card';
import styles from './demos.module.css';
const demoPreviewCard = PreviewCard.createHandle<React.ReactElement>();
const cardContents = {
typography: (
<div className={styles.PopupContent}>
<img
width="224"
height="150"
className={styles.Image}
src="https://images.unsplash.com/photo-1619615391095-dfa29e1672ef?q=80&w=448&h=300"
alt="Station Hofplein signage in Rotterdam, Netherlands"
/>
<p className={styles.Summary}>
<strong>Typography</strong> is the art and science of arranging type.
</p>
</div>
),
design: (
<div className={styles.PopupContent}>
<img
width="241"
height="240"
className={styles.Image}
src="https://upload.wikimedia.org/wikipedia/commons/thumb/b/b7/Braun_ABW30_%28schwarz%29.jpg/250px-Braun_ABW30_%28schwarz%29.jpg"
alt="Braun ABW30"
/>
<p className={styles.Summary}>
A <strong>design</strong> is the concept or proposal for an object, process, or system.
</p>
</div>
),
art: (
<div className={styles.PopupContent}>
<img
width="206"
height="240"
className={styles.Image}
src="https://upload.wikimedia.org/wikipedia/commons/thumb/5/5f/MonaLisa_sfumato.jpeg/250px-MonaLisa_sfumato.jpeg"
alt="Mona Lisa"
/>
<p className={styles.Summary}>
<strong>Art</strong> is a diverse range of cultural activity centered around works utilizing
creative or imaginative talents, which are expected to evoke a worthwhile experience,
generally through an expression of emotional power, conceptual ideas, technical proficiency,
or beauty.
</p>
</div>
),
};
export default function PreviewCardDetachedTriggersControlledDemo() {
const [open, setOpen] = React.useState(false);
const [triggerId, setTriggerId] = React.useState<string | null>(null);
const handleOpenChange = (isOpen: boolean, eventDetails: PreviewCard.Root.ChangeEventDetails) => {
setOpen(isOpen);
setTriggerId(eventDetails.trigger?.id ?? null);
};
return (
<div>
<div className={styles.Container}>
<p className={styles.Paragraph}>
Discover{' '}
<PreviewCard.Trigger
className={styles.Link}
handle={demoPreviewCard}
href="https://en.wikipedia.org/wiki/Typography"
id="trigger-1"
payload={cardContents.typography}
>
typography
</PreviewCard.Trigger>
,{' '}
<PreviewCard.Trigger
className={styles.Link}
handle={demoPreviewCard}
href="https://en.wikipedia.org/wiki/Industrial_design"
id="trigger-2"
payload={cardContents.design}
>
design
</PreviewCard.Trigger>
, or{' '}
<PreviewCard.Trigger
className={styles.Link}
handle={demoPreviewCard}
href="https://en.wikipedia.org/wiki/Art"
id="trigger-3"
payload={cardContents.art}
>
art
</PreviewCard.Trigger>
.
</p>
<button
type="button"
className={styles.Button}
onClick={() => {
setTriggerId('trigger-2');
setOpen(true);
}}
>
Open programmatically
</button>
</div>
<PreviewCard.Root
handle={demoPreviewCard}
open={open}
onOpenChange={handleOpenChange}
triggerId={triggerId}
>
{({ payload }) => (
<PreviewCard.Portal>
<PreviewCard.Positioner sideOffset={8} className={styles.Positioner}>
<PreviewCard.Popup className={styles.Popup}>
<PreviewCard.Arrow className={styles.Arrow}>
<ArrowSvg />
</PreviewCard.Arrow>
{payload}
</PreviewCard.Popup>
</PreviewCard.Positioner>
</PreviewCard.Portal>
)}
</PreviewCard.Root>
</div>
);
}
function ArrowSvg(props: React.ComponentProps<'svg'>) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
className={styles.ArrowFill}
/>
<path
d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
className={styles.ArrowOuterStroke}
/>
<path
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
className={styles.ArrowInnerStroke}
/>
</svg>
);
}
```
```css
/* demos.module.css */
.Positioner {
height: var(--positioner-height);
width: var(--positioner-width);
max-width: var(--available-width);
}
.Popup {
box-sizing: border-box;
width: var(--popup-width, auto);
height: var(--popup-height, auto);
border-radius: 0.5rem;
background-color: canvas;
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
&[data-side='top'] {
bottom: -8px;
rotate: 180deg;
}
&[data-side='bottom'] {
top: -8px;
rotate: 0deg;
}
&[data-side='left'] {
right: -13px;
rotate: 90deg;
}
&[data-side='right'] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.PopupContent {
width: min-content;
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.5rem;
box-sizing: border-box;
}
.Image {
display: block;
border-radius: 0.25rem;
max-width: none;
}
.Summary {
margin: 0;
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--color-gray-900);
text-wrap: pretty;
}
.Container {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: center;
align-items: baseline;
}
.Paragraph {
margin: 0;
font-size: 1rem;
line-height: 1.5rem;
color: var(--color-gray-900);
text-wrap: balance;
}
.Link {
outline: 0;
color: var(--color-blue);
text-decoration-line: none;
text-decoration-thickness: 1px;
text-decoration-color: color-mix(in oklab, var(--color-blue), transparent 40%);
text-underline-offset: 2px;
@media (hover: hover) {
&:hover {
text-decoration-line: underline;
}
}
&[data-popup-open] {
text-decoration-line: underline;
}
&:focus-visible {
border-radius: 0.125rem;
outline: 2px solid var(--color-blue);
text-decoration-line: none;
}
}
.LinkGroup {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
align-items: baseline;
}
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
```
### Animating the Preview Card
You can animate a preview card as it moves between different trigger elements.
This includes animating its position, size, and content.
#### Position and Size
To animate the preview card's position, apply CSS transitions to the `left`, `right`, `top`, and `bottom` properties of the **Positioner** part.
To animate its size, transition the `width` and `height` of the **Popup** part.
#### Content
The preview card also supports content transitions.
This is useful when different triggers display different content within the same preview card.
To enable content animations, wrap the content in the `<PreviewCard.Viewport>` part.
This part provides features to create direction-aware animations.
It renders a `div` with a `data-activation-direction` attribute (`left`, `right`, `up`, or `down`) that indicates the new trigger's position relative to the previous one.
Inside the `<PreviewCard.Viewport>`, the content is further wrapped in `div`s with data attributes to help with styling:
- `data-current`: The currently visible content when no transitions are present or the incoming content.
- `data-previous`: The outgoing content during a transition.
You can use these attributes to style the enter and exit animations.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
'use client';
import * as React from 'react';
import { PreviewCard } from '@base-ui/react/preview-card';
const demoPreviewCard = PreviewCard.createHandle<React.ReactElement>();
const cardContents = {
typography: (
<div className="w-min box-border flex flex-col gap-2 p-2">
<img
width="224"
height="150"
className="block rounded-sm max-w-none"
src="https://images.unsplash.com/photo-1619615391095-dfa29e1672ef?q=80&w=448&h=300"
alt="Station Hofplein signage in Rotterdam, Netherlands"
/>
<p className="m-0 text-sm leading-5 text-gray-900 text-pretty">
<strong>Typography</strong> is the art and science of arranging type.
</p>
</div>
),
design: (
<div className="w-min box-border flex flex-col gap-2 p-2">
<img
width="250"
height="249"
className="block rounded-sm max-w-none"
src="https://upload.wikimedia.org/wikipedia/commons/thumb/b/b7/Braun_ABW30_%28schwarz%29.jpg/250px-Braun_ABW30_%28schwarz%29.jpg"
alt="Braun ABW30"
/>
<p className="m-0 text-sm leading-5 text-gray-900 text-pretty">
A <strong>design</strong> is the concept or proposal for an object, process, or system.
</p>
</div>
),
art: (
<div className="w-min box-border flex flex-col gap-2 p-2">
<img
width="250"
height="290"
className="block rounded-sm max-w-none"
src="https://upload.wikimedia.org/wikipedia/commons/thumb/5/5f/MonaLisa_sfumato.jpeg/250px-MonaLisa_sfumato.jpeg"
alt="Mona Lisa"
/>
<p className="m-0 text-sm leading-5 text-gray-900 text-pretty">
<strong>Art</strong> is a diverse range of cultural activity centered around works utilizing
creative or imaginative talents, which are expected to evoke a worthwhile experience,
generally through an expression of emotional power, conceptual ideas, technical proficiency,
or beauty.
</p>
</div>
),
};
export default function PreviewCardDetachedTriggersFullDemo() {
return (
<div>
<p className="m-0 text-base leading-6 text-gray-900 text-balance">
Discover{' '}
<PreviewCard.Trigger
className="text-blue-800 no-underline decoration-blue-800/60 decoration-1 underline-offset-2 outline-0 hover:underline data-[popup-open]:underline focus-visible:rounded-[2px] focus-visible:no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-800"
handle={demoPreviewCard}
href="https://en.wikipedia.org/wiki/Typography"
payload={cardContents.typography}
>
typography
</PreviewCard.Trigger>
,{' '}
<PreviewCard.Trigger
className="text-blue-800 no-underline decoration-blue-800/60 decoration-1 underline-offset-2 outline-0 hover:underline data-[popup-open]:underline focus-visible:rounded-[2px] focus-visible:no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-800"
handle={demoPreviewCard}
href="https://en.wikipedia.org/wiki/Design"
payload={cardContents.design}
>
design
</PreviewCard.Trigger>
, or{' '}
<PreviewCard.Trigger
className="text-blue-800 no-underline decoration-blue-800/60 decoration-1 underline-offset-2 outline-0 hover:underline data-[popup-open]:underline focus-visible:rounded-[2px] focus-visible:no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-800"
handle={demoPreviewCard}
href="https://en.wikipedia.org/wiki/Art"
payload={cardContents.art}
>
art
</PreviewCard.Trigger>
.
</p>
<PreviewCard.Root handle={demoPreviewCard}>
{({ payload }) => (
<PreviewCard.Portal>
<PreviewCard.Positioner
sideOffset={8}
className="h-[var(--positioner-height)] w-[var(--positioner-width)] max-w-[var(--available-width)] transition-[top,left,right,bottom,transform] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)]"
>
<PreviewCard.Popup className="box-border relative w-[var(--popup-width,auto)] h-[var(--popup-height,auto)] rounded-lg bg-[canvas] origin-[var(--transform-origin)] transition-[width,height,opacity,scale] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] data-[starting-style]:scale-90 data-[starting-style]:opacity-0 data-[ending-style]:scale-90 data-[ending-style]:opacity-0 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 dark:shadow-none dark:outline-gray-300 dark:-outline-offset-1">
<PreviewCard.Arrow className="flex data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180">
<ArrowSvg />
</PreviewCard.Arrow>
<PreviewCard.Viewport className="relative overflow-clip w-full h-full [&_[data-previous]]:w-[var(--popup-width)] [&_[data-previous]]:translate-x-0 [&_[data-previous]]:opacity-100 [&_[data-previous]]:transition-[translate,opacity] [&_[data-previous]]:duration-[350ms,175ms] [&_[data-previous]]:ease-[cubic-bezier(0.22,1,0.36,1)] [&_[data-current]]:w-[var(--popup-width)] [&_[data-current]]:translate-x-0 [&_[data-current]]:opacity-100 [&_[data-current]]:transition-[translate,opacity] [&_[data-current]]:duration-[350ms,175ms] [&_[data-current]]:ease-[cubic-bezier(0.22,1,0.36,1)] data-[activation-direction~='left']:[&_[data-current][data-starting-style]]:-translate-x-[30%] data-[activation-direction~='left']:[&_[data-current][data-starting-style]]:opacity-0 data-[activation-direction~='right']:[&_[data-current][data-starting-style]]:translate-x-[30%] data-[activation-direction~='right']:[&_[data-current][data-starting-style]]:opacity-0 data-[activation-direction~='left']:[&_[data-previous][data-ending-style]]:translate-x-[30%] data-[activation-direction~='left']:[&_[data-previous][data-ending-style]]:opacity-0 data-[activation-direction~='right']:[&_[data-previous][data-ending-style]]:-translate-x-[30%] data-[activation-direction~='right']:[&_[data-previous][data-ending-style]]:opacity-0">
{payload}
</PreviewCard.Viewport>
</PreviewCard.Popup>
</PreviewCard.Positioner>
</PreviewCard.Portal>
)}
</PreviewCard.Root>
</div>
);
}
function ArrowSvg(props: React.ComponentProps<'svg'>) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
className="fill-[canvas]"
/>
<path
d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
className="fill-gray-200 dark:fill-none"
/>
<path
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
className="dark:fill-gray-300"
/>
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Positioner {
--easing: cubic-bezier(0.22, 1, 0.36, 1);
--animation-duration: 0.35s;
height: var(--positioner-height);
width: var(--positioner-width);
max-width: var(--available-width);
transition-property: top, left, right, bottom, transform;
transition-timing-function: var(--easing);
transition-duration: var(--animation-duration);
}
.Popup {
position: relative;
background-color: canvas;
color: var(--color-gray-900);
border-radius: 0.5rem;
transform-origin: var(--transform-origin);
/* These are required to make the size animations work */
width: var(--popup-width, auto);
height: var(--popup-height, auto);
/* width and height are essential for the resize animation; opacity and transform handle the enter/exit animation */
transition-property: width, height, opacity, transform;
transition-timing-function: var(--easing);
transition-duration: var(--animation-duration);
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
&[data-side='top'] {
bottom: -8px;
rotate: 180deg;
}
&[data-side='bottom'] {
top: -8px;
rotate: 0deg;
}
&[data-side='left'] {
right: -13px;
rotate: 90deg;
}
&[data-side='right'] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.Viewport {
/* Required to clip the overflowing content during the slide in/out animations */
position: relative;
overflow: clip;
width: 100%;
height: 100%;
& [data-previous],
& [data-current] {
/* This freezes the width of the content while transitioning.
The 'previous` container receives the width of the previous content, while the `next` container
receives the width of the new content.
*/
width: var(--popup-width);
translate: 0;
opacity: 1;
transition:
translate var(--animation-duration) var(--easing),
opacity calc(var(--animation-duration) / 2) var(--easing);
}
&[data-activation-direction~='left'] [data-current][data-starting-style] {
translate: -30% 0;
opacity: 0;
}
&[data-activation-direction~='right'] [data-current][data-starting-style] {
translate: 30% 0;
opacity: 0;
}
&[data-activation-direction~='left'] [data-previous][data-ending-style] {
translate: 30% 0;
opacity: 0;
}
&[data-activation-direction~='right'] [data-previous][data-ending-style] {
translate: -30% 0;
opacity: 0;
}
}
.PopupContent {
width: min-content;
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.5rem;
box-sizing: border-box;
}
.Image {
display: block;
border-radius: 0.25rem;
max-width: none;
}
.Summary {
margin: 0;
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--color-gray-900);
text-wrap: pretty;
}
.Paragraph {
margin: 0;
font-size: 1rem;
line-height: 1.5rem;
color: var(--color-gray-900);
text-wrap: balance;
}
.Link {
outline: 0;
color: var(--color-blue);
text-decoration-line: none;
text-decoration-thickness: 1px;
text-decoration-color: color-mix(in oklab, var(--color-blue), transparent 40%);
text-underline-offset: 2px;
@media (hover: hover) {
&:hover {
text-decoration-line: underline;
}
}
&[data-popup-open] {
text-decoration-line: underline;
}
&:focus-visible {
border-radius: 0.125rem;
outline: 2px solid var(--color-blue);
text-decoration-line: none;
}
}
```
```tsx
/* index.tsx */
'use client';
import * as React from 'react';
import { PreviewCard } from '@base-ui/react/preview-card';
import styles from './index.module.css';
const demoPreviewCard = PreviewCard.createHandle<React.ReactElement>();
const cardContents = {
typography: (
<div className={styles.PopupContent}>
<img
width="224"
height="150"
className={styles.Image}
src="https://images.unsplash.com/photo-1619615391095-dfa29e1672ef?q=80&w=448&h=300"
alt="Station Hofplein signage in Rotterdam, Netherlands"
/>
<p className={styles.Summary}>
<strong>Typography</strong> is the art and science of arranging type.
</p>
</div>
),
design: (
<div className={styles.PopupContent}>
<img
width="250"
height="249"
className={styles.Image}
src="https://upload.wikimedia.org/wikipedia/commons/thumb/b/b7/Braun_ABW30_%28schwarz%29.jpg/250px-Braun_ABW30_%28schwarz%29.jpg"
alt="Braun ABW30"
/>
<p className={styles.Summary}>
A <strong>design</strong> is the concept or proposal for an object, process, or system.
</p>
</div>
),
art: (
<div className={styles.PopupContent}>
<img
width="250"
height="290"
className={styles.Image}
src="https://upload.wikimedia.org/wikipedia/commons/thumb/5/5f/MonaLisa_sfumato.jpeg/250px-MonaLisa_sfumato.jpeg"
alt="Mona Lisa"
/>
<p className={styles.Summary}>
<strong>Art</strong> is a diverse range of cultural activity centered around works utilizing
creative or imaginative talents, which are expected to evoke a worthwhile experience,
generally through an expression of emotional power, conceptual ideas, technical proficiency,
or beauty.
</p>
</div>
),
};
export default function PreviewCardDetachedTriggersFullDemo() {
return (
<div>
<p className={styles.Paragraph}>
Discover{' '}
<PreviewCard.Trigger
className={styles.Link}
handle={demoPreviewCard}
href="https://en.wikipedia.org/wiki/Typography"
payload={cardContents.typography}
>
typography
</PreviewCard.Trigger>
,{' '}
<PreviewCard.Trigger
className={styles.Link}
handle={demoPreviewCard}
href="https://en.wikipedia.org/wiki/Design"
payload={cardContents.design}
>
design
</PreviewCard.Trigger>
, or{' '}
<PreviewCard.Trigger
className={styles.Link}
handle={demoPreviewCard}
href="https://en.wikipedia.org/wiki/Art"
payload={cardContents.art}
>
art
</PreviewCard.Trigger>
.
</p>
<PreviewCard.Root handle={demoPreviewCard}>
{({ payload }) => (
<PreviewCard.Portal>
<PreviewCard.Positioner sideOffset={8} className={styles.Positioner}>
<PreviewCard.Popup className={styles.Popup}>
<PreviewCard.Arrow className={styles.Arrow}>
<ArrowSvg />
</PreviewCard.Arrow>
<PreviewCard.Viewport className={styles.Viewport}>{payload}</PreviewCard.Viewport>
</PreviewCard.Popup>
</PreviewCard.Positioner>
</PreviewCard.Portal>
)}
</PreviewCard.Root>
</div>
);
}
function ArrowSvg(props: React.ComponentProps<'svg'>) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
className={styles.ArrowFill}
/>
<path
d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
className={styles.ArrowOuterStroke}
/>
<path
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
className={styles.ArrowInnerStroke}
/>
</svg>
);
}
```
## API reference
### Root
Groups all parts of the preview card.
Doesn’t render its own HTML element.
**Root Props:**
| Prop | Type | Default | Description |
| :------------------------------------------------------------ | :----------------------------------------------------------------------------- | :------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| defaultOpen | `boolean` | `false` | Whether the preview card is initially open.To render a controlled preview card, use the `open` prop instead. |
| open | `boolean` | - | Whether the preview card is currently open. |
| onOpenChange | `((open: boolean, eventDetails: PreviewCard.Root.ChangeEventDetails) => void)` | - | Event handler called when the preview card is opened or closed. |
| actionsRef | `RefObject<PreviewCard.Root.Actions \| null>` | - | A ref to imperative actions.\* `unmount`: Ends the closing phase of the preview card after an externally controlled closing animation finishes.
Call `preventUnmountOnClose()` in `onOpenChange` first, otherwise the preview card completes closing on its own.
Whether it leaves the DOM is decided by `keepMounted` on the portal. |
| \* `close`: Closes the preview card imperatively when called. |
| defaultTriggerId | `string \| null` | - | ID of the trigger that the preview card is associated with.
This is useful in conjunction with the `defaultOpen` prop to create an initially open preview card. |
| handle | `PreviewCard.Handle<Payload>` | - | A handle to associate the preview card with a trigger.
If specified, allows external triggers to control the card's open state.
Can be created with the PreviewCard.createHandle() method. |
| onOpenChangeComplete | `((open: boolean) => void)` | - | Event handler called after any animations complete when the preview card is opened or closed. |
| triggerId | `string \| null` | - | ID of the trigger that the preview card is associated with.
This is useful in conjuntion with the `open` prop to create a controlled preview card.
There's no need to specify this prop when the preview card is uncontrolled (i.e. when the `open` prop is not set). |
| children | `ReactNode \| PayloadChildRenderFunction<Payload>` | - | The content of the preview card.
This can be a regular React node or a render function that receives the `payload` of the active trigger. |
### Trigger
A link that opens the preview card.
Renders an `<a>` element.
**Trigger Props:**
| Prop | Type | Default | Description |
| :--------- | :--------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| handle | `PreviewCard.Handle<Payload>` | - | A handle to associate the trigger with a preview card. |
| payload | `Payload` | - | A payload to pass to the preview card when it is opened. |
| delay | `number` | `600` | How long to wait before the preview card opens. Specified in milliseconds. |
| closeDelay | `number` | `300` | How long to wait before closing the preview card. Specified in milliseconds. |
| className | `string \| ((state: PreviewCard.Trigger.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: PreviewCard.Trigger.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: PreviewCard.Trigger.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Trigger Data Attributes:**
| Attribute | Type | Description |
| :-------------- | :--- | :--------------------------------------------------- |
| data-popup-open | - | Present when the corresponding preview card is open. |
### Portal
A portal element that moves the popup to a different part of the DOM.
By default, the portal element is appended to `<body>`.
Renders a `<div>` element.
**Portal Props:**
| Prop | Type | Default | Description |
| :---------- | :-------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| container | `HTMLElement \| ShadowRoot \| RefObject<HTMLElement \| ShadowRoot \| null> \| null` | - | A parent element to render the portal element into. |
| className | `string \| ((state: PreviewCard.Portal.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: PreviewCard.Portal.State) => CSSProperties \| undefined)` | - | - |
| keepMounted | `boolean` | `false` | Whether to keep the portal mounted in the DOM while the popup is hidden. |
| render | `ReactElement \| ((props: HTMLProps, state: PreviewCard.Portal.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Backdrop
An overlay displayed beneath the popup.
Renders a `<div>` element.
**Backdrop Props:**
| Prop | Type | Default | Description |
| :-------- | :---------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: PreviewCard.Backdrop.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: PreviewCard.Backdrop.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: PreviewCard.Backdrop.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Backdrop Data Attributes:**
| Attribute | Type | Description |
| :------------------ | :--- | :---------------------------------------------- |
| data-open | - | Present when the preview card is open. |
| data-closed | - | Present when the preview card is closed. |
| data-starting-style | - | Present when the preview card is animating in. |
| data-ending-style | - | Present when the preview card is animating out. |
### Positioner
Positions the popup against the trigger.
Renders a `<div>` element.
**Positioner Props:**
| Prop | Type | Default | Description |
| :-------------------- | :------------------------- | :--------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| disableAnchorTracking | `boolean` | `false` | Whether to disable the popup from tracking any layout shift of its positioning anchor. |
| align | `Align` | `'center'` | How to align the popup relative to the specified side. |
| alignOffset | `number \| OffsetFunction` | `0` | Additional offset along the alignment axis in pixels.
Also accepts a function that returns the offset to read the dimensions of the anchor
and positioner elements, along with its side and alignment.The function takes a `data` object parameter with the following properties:\* `data.anchor`: the dimensions of the anchor element with properties `width` and `height`. |
- `data.positioner`: the dimensions of the positioner element with properties `width` and `height`.
- `data.side`: which side of the anchor element the positioner is aligned against.
- `data.align`: how the positioner is aligned relative to the specified side. |
| side | `Side` | `'bottom'` | Which side of the anchor element to align the popup against.
May automatically change to avoid collisions. |
| sideOffset | `number \| OffsetFunction` | `0` | Distance between the anchor and the popup in pixels.
Also accepts a function that returns the distance to read the dimensions of the anchor
and positioner elements, along with its side and alignment.The function takes a `data` object parameter with the following properties:\* `data.anchor`: the dimensions of the anchor element with properties `width` and `height`.
- `data.positioner`: the dimensions of the positioner element with properties `width` and `height`.
- `data.side`: which side of the anchor element the positioner is aligned against.
- `data.align`: how the positioner is aligned relative to the specified side. |
| arrowPadding | `number` | `5` | Minimum distance to maintain between the arrow and the edges of the popup.Use it to prevent the arrow element from hanging out of the rounded corners of a popup. |
| anchor | `Element \| RefObject<Element \| null> \| VirtualElement \| (() => Element \| VirtualElement \| null) \| null` | - | An element to position the popup against.
By default, the popup will be positioned against the trigger. |
| collisionAvoidance | `CollisionAvoidance` | - | Determines how to handle collisions when positioning the popup. |
| collisionBoundary | `Boundary` | `'clipping-ancestors'` | An element or a rectangle that delimits the area that the popup is confined to. |
| collisionPadding | `Padding` | `5` | Additional space to maintain from the edge of the collision boundary. |
| sticky | `boolean` | `false` | Whether to maintain the popup in the viewport after
the anchor element was scrolled out of view. |
| positionMethod | `'fixed' \| 'absolute'` | `'absolute'` | Determines which CSS `position` property to use. |
| className | `string \| ((state: PreviewCard.Positioner.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: PreviewCard.Positioner.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: PreviewCard.Positioner.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Positioner Data Attributes:**
| Attribute | Type | Description |
| :----------------- | :------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| data-open | - | Present when the preview card is open. |
| data-closed | - | Present when the preview card is closed. |
| data-anchor-hidden | - | Present when the anchor is hidden. |
| data-align | `'start' \| 'center' \| 'end'` | Indicates how the popup is aligned relative to specified side. |
| data-side | `'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the popup is positioned relative to the trigger. |
**Positioner CSS Variables:**
| Variable | Type | Default | Description |
| :----------------- | :------- | :------ | :------------------------------------------------------------------------------------- |
| --anchor-height | `number` | - | The anchor's height. |
| --anchor-width | `number` | - | The anchor's width. |
| --available-height | `number` | - | The available height between the trigger and the edge of the viewport. |
| --available-width | `number` | - | The available width between the trigger and the edge of the viewport. |
| --transform-origin | `string` | - | The coordinates that this element is anchored to. Used for animations and transitions. |
### Popup
A container for the preview card contents.
Renders a `<div>` element.
**Popup Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: PreviewCard.Popup.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: PreviewCard.Popup.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: PreviewCard.Popup.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Popup Data Attributes:**
| Attribute | Type | Description |
| :------------------ | :------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| data-open | - | Present when the preview card is open. |
| data-closed | - | Present when the preview card is closed. |
| data-align | `'start' \| 'center' \| 'end'` | Indicates how the popup is aligned relative to specified side. |
| data-side | `'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the popup is positioned relative to the trigger. |
| data-starting-style | - | Present when the preview card is animating in. |
| data-ending-style | - | Present when the preview card is animating out. |
### Viewport
A viewport for displaying content transitions.
This component is only required if one popup can be opened by multiple triggers, its content change based on the trigger
and switching between them is animated.
Renders a `<div>` element.
**Viewport Props:**
| Prop | Type | Default | Description |
| :-------- | :---------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| children | `ReactNode` | - | The content to render inside the transition container. |
| className | `string \| ((state: PreviewCard.Viewport.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: PreviewCard.Viewport.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: PreviewCard.Viewport.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Viewport Data Attributes:**
| Attribute | Type | Description |
| :------------------------ | :----------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| data-activation-direction | `` `${'left' \| 'right'} {'top' \| 'bottom'}` `` | Indicates the direction from which the popup was activated.
This can be used to create directional animations based on how the popup was triggered.
Contains space-separated values for both horizontal and vertical axes. |
| data-current | - | Applied to the direct child of the viewport when no transitions are present or the new content when it's entering. |
| data-instant | `'delay' \| 'dismiss' \| 'focus'` | Present if animations should be instant. |
| data-previous | - | Applied to the direct child of the viewport that contains the exiting content when transitions are present. |
| data-transitioning | - | Indicates that the viewport is currently transitioning between old and new content. |
**Viewport CSS Variables:**
| Variable | Type | Default | Description |
| :------------- | :--- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| --popup-height | - | - | The height of the parent popup.
This variable is placed on the 'previous' container and stores the height of the popup when the previous content was rendered.
It can be used to freeze the dimensions of the popup when animating between different content. |
| --popup-width | - | - | The width of the parent popup.
This variable is placed on the 'previous' container and stores the width of the popup when the previous content was rendered.
It can be used to freeze the dimensions of the popup when animating between different content. |
### Arrow
Displays an element positioned against the preview card anchor.
Renders a `<div>` element.
**Arrow Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: PreviewCard.Arrow.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: PreviewCard.Arrow.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: PreviewCard.Arrow.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Arrow Data Attributes:**
| Attribute | Type | Description |
| :-------------- | :------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| data-open | - | Present when the preview card is open. |
| data-closed | - | Present when the preview card is closed. |
| data-uncentered | - | Present when the preview card arrow is uncentered. |
| data-align | `'start' \| 'center' \| 'end'` | Indicates how the popup is aligned relative to specified side. |
| data-side | `'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the popup is positioned relative to the trigger. |
references/progress.md
---
title: Progress
subtitle: Displays the status of a task that takes a long time.
description: A high-quality, unstyled React progress bar component that displays the status of a task that takes a long time.
---
# Progress
A high-quality, unstyled React progress bar component that displays the status of a task that takes a long time.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Progress } from "@base-ui/react/progress";
export default function ExampleProgress() {
const [value, setValue] = React.useState(20);
// Simulate changes
React.useEffect(() => {
const interval = setInterval(() => {
setValue((current) => Math.min(100, Math.round(current + Math.random() * 25)));
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<Progress.Root className="grid w-48 grid-cols-2 gap-y-2" value={value}>
<Progress.Label className="text-sm font-medium text-gray-900">Export data</Progress.Label>
<Progress.Value className="col-start-2 text-right text-sm text-gray-900" />
<Progress.Track className="col-span-full h-1 overflow-hidden rounded bg-gray-200 shadow-[inset_0_0_0_1px] shadow-gray-200">
<Progress.Indicator className="block bg-gray-500 transition-all duration-500" />
</Progress.Track>
</Progress.Root>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Progress {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 0.25rem;
grid-row-gap: 0.5rem;
width: 12rem;
}
.Label {
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
color: var(--color-gray-900);
}
.Value {
grid-column-start: 2;
margin: 0;
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--color-gray-900);
text-align: right;
}
.Track {
grid-column: 1 / 3;
overflow: hidden;
background-color: var(--color-gray-200);
box-shadow: inset 0 0 0 1px var(--color-gray-200);
height: 0.25rem;
border-radius: 0.25rem;
}
.Indicator {
display: block;
background-color: var(--color-gray-500);
transition: width 500ms;
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Progress } from "@base-ui/react/progress";
import styles from "./index.module.css";
export default function ExampleProgress() {
const [value, setValue] = React.useState(20);
// Simulate changes
React.useEffect(() => {
const interval = setInterval(() => {
setValue((current) => Math.min(100, Math.round(current + Math.random() * 25)));
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<Progress.Root className={styles.Progress} value={value}>
<Progress.Label className={styles.Label}>Export data</Progress.Label>
<Progress.Value className={styles.Value} />
<Progress.Track className={styles.Track}>
<Progress.Indicator className={styles.Indicator} />
</Progress.Track>
</Progress.Root>
);
}
```
## Anatomy
Import the component and assemble its parts:
```jsx title="Anatomy"
import { Progress } from "@base-ui/react/progress";
<Progress.Root>
<Progress.Label />
<Progress.Track>
<Progress.Indicator />
</Progress.Track>
<Progress.Value />
</Progress.Root>;
```
## API reference
### Root
Groups all parts of the progress bar and provides the task completion status to screen readers.
Renders a `<div>` element.
**Root Props:**
| Prop | Type | Default | Description |
| :--------------- | :--------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| value | `number \| null` | `null` | The current value. The component is indeterminate when value is `null`. |
| aria-valuetext | `string` | - | A string value that provides a user-friendly name for `aria-valuenow`, the current value of the meter. |
| getAriaValueText | `((formattedValue: string \| null, value: number \| null) => string)` | - | Accepts a function which returns a string value that provides a human-readable text alternative for the current value of the progress bar. |
| locale | `Intl.LocalesArgument` | - | The locale used by `Intl.NumberFormat` when formatting the value.
Defaults to the user's runtime locale. |
| min | `number` | `0` | The minimum value. |
| max | `number` | `100` | The maximum value. |
| format | `Intl.NumberFormatOptions` | - | Options to format the value. |
| className | `string \| ((state: Progress.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Progress.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Progress.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Root Data Attributes:**
| Attribute | Type | Description |
| :----------------- | :--- | :--------------------------------------------------- |
| data-complete | - | Present when the progress has completed. |
| data-indeterminate | - | Present when the progress is in indeterminate state. |
| data-progressing | - | Present while the progress is progressing. |
### Track
Contains the progress bar indicator.
Renders a `<div>` element.
**Track Props:**
| Prop | Type | Default | Description |
| :-------- | :--------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Progress.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Progress.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Progress.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Track Data Attributes:**
| Attribute | Type | Description |
| :----------------- | :--- | :--------------------------------------------------- |
| data-complete | - | Present when the progress has completed. |
| data-indeterminate | - | Present when the progress is in indeterminate state. |
| data-progressing | - | Present while the progress is progressing. |
### Indicator
Visualizes the completion status of the task.
Renders a `<div>` element.
**Indicator Props:**
| Prop | Type | Default | Description |
| :-------- | :--------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Progress.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Progress.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Progress.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Indicator Data Attributes:**
| Attribute | Type | Description |
| :----------------- | :--- | :--------------------------------------------------- |
| data-complete | - | Present when the progress has completed. |
| data-indeterminate | - | Present when the progress is in indeterminate state. |
| data-progressing | - | Present while the progress is progressing. |
### Value
A text label displaying the current value.
Renders a `<span>` element.
**Value Props:**
| Prop | Type | Default | Description |
| :-------- | :--------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| children | `((formattedValue: string \| null, value: number \| null) => ReactNode) \| null` | - | - |
| className | `string \| ((state: Progress.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Progress.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Progress.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Value Data Attributes:**
| Attribute | Type | Description |
| :----------------- | :--- | :--------------------------------------------------- |
| data-complete | - | Present when the progress has completed. |
| data-indeterminate | - | Present when the progress is in indeterminate state. |
| data-progressing | - | Present while the progress is progressing. |
### Label
An accessible label for the progress bar.
Renders a `<span>` element.
**Label Props:**
| Prop | Type | Default | Description |
| :-------- | :--------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Progress.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Progress.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Progress.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Label Data Attributes:**
| Attribute | Type | Description |
| :----------------- | :--- | :--------------------------------------------------- |
| data-complete | - | Present when the progress has completed. |
| data-indeterminate | - | Present when the progress is in indeterminate state. |
| data-progressing | - | Present while the progress is progressing. |
references/radio.md
---
title: Radio
subtitle: An easily stylable radio button component.
description: A high-quality, unstyled React radio button component that is easy to style.
---
# Radio
A high-quality, unstyled React radio button component that is easy to style.
## v1.9.0 notes
- `aria-label` is preferred over an associated label when both are present, so an explicit `aria-label` on the radio or radio group wins.
- Arrow keys that the group ignores no longer select a value when focus returns to the group.
- `inputRef` forwarding and cleanup are fixed for both the radio and the radio group.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Radio } from "@base-ui/react/radio";
import { RadioGroup } from "@base-ui/react/radio-group";
export default function ExampleRadioGroup() {
const id = React.useId();
return (
<RadioGroup aria-labelledby={id} defaultValue="fuji-apple" className="flex flex-col items-start gap-1 text-gray-900">
<div className="font-medium" id={id}>
Best apple
</div>
<label className="flex items-center gap-2">
<Radio.Root value="fuji-apple" className="flex size-5 items-center justify-center rounded-full focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-800 data-[checked]:bg-gray-900 data-[unchecked]:border data-[unchecked]:border-gray-300">
<Radio.Indicator className="flex before:size-2 before:rounded-full before:bg-gray-50 data-[unchecked]:hidden" />
</Radio.Root>
Fuji
</label>
<label className="flex items-center gap-2">
<Radio.Root value="gala-apple" className="flex size-5 items-center justify-center rounded-full focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-800 data-[checked]:bg-gray-900 data-[unchecked]:border data-[unchecked]:border-gray-300">
<Radio.Indicator className="flex before:size-2 before:rounded-full before:bg-gray-50 data-[unchecked]:hidden" />
</Radio.Root>
Gala
</label>
<label className="flex items-center gap-2">
<Radio.Root value="granny-smith-apple" className="flex size-5 items-center justify-center rounded-full focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-800 data-[checked]:bg-gray-900 data-[unchecked]:border data-[unchecked]:border-gray-300">
<Radio.Indicator className="flex before:size-2 before:rounded-full before:bg-gray-50 data-[unchecked]:hidden" />
</Radio.Root>
Granny Smith
</label>
</RadioGroup>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.RadioGroup {
display: flex;
flex-direction: column;
align-items: start;
gap: 0.25rem;
color: var(--color-gray-900);
}
.Caption {
font-weight: 500;
}
.Item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.Radio {
box-sizing: border-box;
display: flex;
width: 1.25rem;
height: 1.25rem;
align-items: center;
justify-content: center;
border-radius: 100%;
outline: 0;
padding: 0;
margin: 0;
border: none;
&[data-unchecked] {
border: 1px solid var(--color-gray-300);
background-color: transparent;
}
&[data-checked] {
background-color: var(--color-gray-900);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: 2px;
}
}
.Indicator {
display: flex;
align-items: center;
justify-content: center;
&[data-unchecked] {
display: none;
}
&::before {
content: "";
border-radius: 100%;
width: 0.5rem;
height: 0.5rem;
background-color: var(--color-gray-50);
}
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Radio } from "@base-ui/react/radio";
import { RadioGroup } from "@base-ui/react/radio-group";
import styles from "./index.module.css";
export default function ExampleRadioGroup() {
const id = React.useId();
return (
<RadioGroup aria-labelledby={id} defaultValue="fuji-apple" className={styles.RadioGroup}>
<div className={styles.Caption} id={id}>
Best apple
</div>
<label className={styles.Item}>
<Radio.Root value="fuji-apple" className={styles.Radio}>
<Radio.Indicator className={styles.Indicator} />
</Radio.Root>
Fuji
</label>
<label className={styles.Item}>
<Radio.Root value="gala-apple" className={styles.Radio}>
<Radio.Indicator className={styles.Indicator} />
</Radio.Root>
Gala
</label>
<label className={styles.Item}>
<Radio.Root value="granny-smith-apple" className={styles.Radio}>
<Radio.Indicator className={styles.Indicator} />
</Radio.Root>
Granny Smith
</label>
</RadioGroup>
);
}
```
## Usage guidelines
- **Form controls must have an accessible name**: It can be created using `<label>` elements, or the `Field` and `Fieldset` components. See [Labeling a radio group](#labeling-a-radio-group) and the [forms guide](/react/handbook/forms.md).
## Anatomy
Radio is always placed within Radio Group. Import the components and place them together:
```jsx title="Anatomy"
import { Radio } from "@base-ui/react/radio";
import { RadioGroup } from "@base-ui/react/radio-group";
<RadioGroup>
<Radio.Root>
<Radio.Indicator />
</Radio.Root>
</RadioGroup>;
```
## Examples
### Labeling a radio group
A visible label for the group is created by specifying `aria-labelledby` on `<RadioGroup>` to reference the `id` of a sibling element that contains the group label text.
```tsx title="Using aria-labelledby to label a radio group"
<div id="storage-type-label">Storage type</div>
<RadioGroup aria-labelledby="storage-type-label">{/* ... */}</RadioGroup>
```
For individual radio buttons, use an enclosing `<label>` element that wraps each `<Radio.Root>`. An enclosing label is announced to screen readers when focus is on the control, and ensures that clicking on any gaps between the label and radio still toggles it.
```tsx title="Using an enclosing label to label a radio button" {1,4}
<label>
<Radio.Root value="ssd" />
SSD
</label>
```
The [Field](/react/components/field.md) and [Fieldset](/react/components/fieldset.md) components eliminate the need to track `id` or `aria-labelledby` associations manually. They support both enclosing and sibling labeling patterns, along with a UX improvement to prevent double clicks from selecting the label text.
1. **Group label**: Since the group contains multiple controls, it can be labeled as a fieldset. Use [Fieldset](/react/components/fieldset.md) by replacing the `<Fieldset.Root>` element with `<RadioGroup>` via the `render` prop. `<Fieldset.Legend>` provides the visible label for the group.
2. **Individual label**: Label an individual radio using an enclosing `<Field.Label>`. Enclosing labels ensure that clicking on any gaps between the label and radio still toggles it.
```tsx title="Using Field and Fieldset to label a radio group"
<Field.Root>
<Fieldset.Root render={<RadioGroup />}>
<Fieldset.Legend>Storage type</Fieldset.Legend>
<Field.Label>
<Radio.Root value="ssd" />
SSD
</Field.Label>
<Field.Label>
<Radio.Root value="hdd" />
HDD
</Field.Label>
</Fieldset.Root>
</Field.Root>
```
### Form integration
To use a radio group in a form, pass the radio group's `name` to `<Field.Root>`:
```tsx title="Using Radio Group in a form" {2}
<Form>
<Field.Root name="storageType">
<Fieldset.Root render={<RadioGroup />}>
<Fieldset.Legend>Storage type</Fieldset.Legend>
<Field.Label>
<Radio.Root value="ssd" />
SSD
</Field.Label>
<Field.Label>
<Radio.Root value="hdd" />
HDD
</Field.Label>
</Fieldset.Root>
</Field.Root>
</Form>
```
## API reference
### RadioGroup
Provides a shared state to a series of radio buttons. Renders a `<div>` element.
**RadioGroup Props:**
| Prop | Type | Default | Description |
| :------------ | :------------------------------------------------------------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | `string` | - | Identifies the field when a form is submitted. |
| defaultValue | `any` | - | The uncontrolled value of the radio button that should be initially selected. To render a controlled radio group, use the `value` prop instead. |
| value | `any` | - | The controlled value of the radio item that should be currently selected. To render an uncontrolled radio group, use the `defaultValue` prop instead. |
| onValueChange | `((value: any, eventDetails: Radio.Group.ChangeEventDetails) => void)` | - | Callback fired when the value changes. |
| disabled | `boolean` | `false` | Whether the component should ignore user interaction. |
| readOnly | `boolean` | `false` | Whether the user should be unable to select a different radio button in the group. |
| required | `boolean` | `false` | Whether the user must choose a value before submitting a form. |
| inputRef | `Ref<HTMLInputElement>` | - | A ref to access the hidden input element. |
| className | `string \| ((state: RadioGroup.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: RadioGroup.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: RadioGroup.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**RadioGroup Data Attributes:**
| Attribute | Type | Description |
| :------------ | :--- | :---------------------------------------- |
| data-disabled | - | Present when the radio group is disabled. |
### Root
Represents the radio button itself.
Renders a `<span>` element and a hidden `<input>` beside.
**Root Props:**
| Prop | Type | Default | Description |
| :----------- | :------------------------------------------------------------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| value | `any` | - | The unique identifying value of the radio in a group. |
| nativeButton | `boolean` | `false` | Whether the component renders a native `<button>` element when replacing it
via the `render` prop.
Set to `true` if the rendered element is a native button. |
| disabled | `boolean` | - | Whether the component should ignore user interaction. |
| readOnly | `boolean` | - | Whether the user should be unable to select the radio button. |
| required | `boolean` | - | Whether the user must choose a value before submitting a form. |
| inputRef | `Ref<HTMLInputElement>` | - | A ref to access the hidden input element. |
| className | `string \| ((state: Radio.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Radio.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Radio.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Root Data Attributes:**
| Attribute | Type | Description |
| :------------- | :--- | :----------------------------------------------------------------------- |
| data-checked | - | Present when the radio is checked. |
| data-unchecked | - | Present when the radio is not checked. |
| data-disabled | - | Present when the radio is disabled. |
| data-readonly | - | Present when the radio is readonly. |
| data-required | - | Present when the radio is required. |
| data-valid | - | Present when the radio is in valid state (when wrapped in Field.Root). |
| data-invalid | - | Present when the radio is in invalid state (when wrapped in Field.Root). |
| data-dirty | - | Present when the radio's value has changed (when wrapped in Field.Root). |
| data-touched | - | Present when the radio has been touched (when wrapped in Field.Root). |
| data-filled | - | Present when the radio is checked (when wrapped in Field.Root). |
| data-focused | - | Present when the radio is focused (when wrapped in Field.Root). |
### Indicator
Indicates whether the radio button is selected.
Renders a `<span>` element.
**Indicator Props:**
| Prop | Type | Default | Description |
| :---------- | :----------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Radio.Indicator.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Radio.Indicator.State) => CSSProperties \| undefined)` | - | - |
| keepMounted | `boolean` | `false` | Whether to keep the HTML element in the DOM when the radio button is inactive. |
| render | `ReactElement \| ((props: HTMLProps, state: Radio.Indicator.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Indicator Data Attributes:**
| Attribute | Type | Description |
| :------------- | :--- | :----------------------------------------------------------------------- |
| data-checked | - | Present when the radio is checked. |
| data-unchecked | - | Present when the radio is not checked. |
| data-disabled | - | Present when the radio is disabled. |
| data-readonly | - | Present when the radio is readonly. |
| data-required | - | Present when the radio is required. |
| data-valid | - | Present when the radio is in valid state (when wrapped in Field.Root). |
| data-invalid | - | Present when the radio is in invalid state (when wrapped in Field.Root). |
| data-dirty | - | Present when the radio's value has changed (when wrapped in Field.Root). |
| data-touched | - | Present when the radio has been touched (when wrapped in Field.Root). |
| data-filled | - | Present when the radio is checked (when wrapped in Field.Root). |
| data-focused | - | Present when the radio is focused (when wrapped in Field.Root). |
references/scroll-area.md
---
title: Scroll Area
subtitle: A native scroll container with custom scrollbars.
description: A high-quality, unstyled React scroll area that provides a native scroll container with custom scrollbars.
---
# Scroll Area
A high-quality, unstyled React scroll area that provides a native scroll container with custom scrollbars.
## v1.4.1 notes
- Highlight clearing on pointer leave is more reliable when interactive items are clipped by a scroll container. If you embed menus, listboxes, or other highlighted items inside scrollable regions, retest hover exit behavior before carrying old workarounds forward.
## v1.5.0 notes
- RTL behavior is corrected in the `1.5.0` line. Re-test custom scrollbar placement and direction-sensitive CSS before preserving earlier fixes.
## v1.9.0 notes
- The viewport no longer carries the presentational viewport role. Retest any screen-reader expectations that relied on it.
## Demo
#### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import { ScrollArea } from "@base-ui/react/scroll-area";
export default function ExampleScrollArea() {
return (
<ScrollArea.Root className="h-[8.5rem] w-96 max-w-[calc(100vw-8rem)]">
<ScrollArea.Viewport className="h-full rounded-md outline outline-1 -outline-offset-1 outline-gray-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-800">
<ScrollArea.Content className="flex flex-col gap-4 py-3 pr-6 pl-4 text-sm leading-[1.375rem] text-gray-900">
<p>Vernacular architecture is building done outside any academic tradition, and without professional guidance. It is not a particular architectural movement or style, but rather a broad category, encompassing a wide range and variety of building types, with differing methods of construction, from around the world, both historical and extant and classical and modern. Vernacular architecture constitutes 95% of the world's built environment, as estimated in 1995 by Amos Rapoport, as measured against the small percentage of new buildings every year designed by architects and built by engineers.</p>
<p>This type of architecture usually serves immediate, local needs, is constrained by the materials available in its particular region and reflects local traditions and cultural practices. The study of vernacular architecture does not examine formally schooled architects, but instead that of the design skills and tradition of local builders, who were rarely given any attribution for the work. More recently, vernacular architecture has been examined by designers and the building industry in an effort to be more energy conscious with contemporary design and construction—part of a broader interest in sustainable design.</p>
</ScrollArea.Content>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar className="m-2 flex w-1 justify-center rounded bg-gray-200 opacity-0 transition-opacity pointer-events-none data-[hovering]:opacity-100 data-[hovering]:delay-0 data-[hovering]:pointer-events-auto data-[scrolling]:opacity-100 data-[scrolling]:duration-0 data-[scrolling]:pointer-events-auto">
<ScrollArea.Thumb className="w-full rounded bg-gray-500" />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
);
}
```
#### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.ScrollArea {
box-sizing: border-box;
width: 24rem;
height: 8.5rem;
max-width: calc(100vw - 8rem);
}
.Viewport {
height: 100%;
border-radius: 0.375rem;
outline: 1px solid var(--color-gray-200);
outline-offset: -1px;
&:focus-visible {
outline: 2px solid var(--color-blue);
}
}
.Content {
display: flex;
flex-direction: column;
gap: 1rem;
padding-block: 0.75rem;
padding-left: 1rem;
padding-right: 1.5rem;
}
.Paragraph {
margin: 0;
font-size: 0.875rem;
line-height: 1.375rem;
color: var(--color-gray-900);
}
.Scrollbar {
display: flex;
justify-content: center;
background-color: var(--color-gray-200);
width: 0.25rem;
border-radius: 0.375rem;
margin: 0.5rem;
opacity: 0;
transition: opacity 150ms;
pointer-events: none;
&[data-scrolling] {
transition-duration: 0ms;
}
&[data-hovering],
&[data-scrolling] {
opacity: 1;
pointer-events: auto;
}
&::before {
content: "";
position: absolute;
width: 1.25rem;
height: 100%;
}
}
.Thumb {
width: 100%;
border-radius: inherit;
background-color: var(--color-gray-500);
}
```
```tsx
/* index.tsx */
import { ScrollArea } from "@base-ui/react/scroll-area";
import styles from "./index.module.css";
export default function ExampleScrollArea() {
return (
<ScrollArea.Root className={styles.ScrollArea}>
<ScrollArea.Viewport className={styles.Viewport}>
<ScrollArea.Content className={styles.Content}>
<p className={styles.Paragraph}>Vernacular architecture is building done outside any academic tradition, and without professional guidance. It is not a particular architectural movement or style, but rather a broad category, encompassing a wide range and variety of building types, with differing methods of construction, from around the world, both historical and extant and classical and modern. Vernacular architecture constitutes 95% of the world's built environment, as estimated in 1995 by Amos Rapoport, as measured against the small percentage of new buildings every year designed by architects and built by engineers.</p>
<p className={styles.Paragraph}>This type of architecture usually serves immediate, local needs, is constrained by the materials available in its particular region and reflects local traditions and cultural practices. The study of vernacular architecture does not examine formally schooled architects, but instead that of the design skills and tradition of local builders, who were rarely given any attribution for the work. More recently, vernacular architecture has been examined by designers and the building industry in an effort to be more energy conscious with contemporary design and construction—part of a broader interest in sustainable design.</p>
</ScrollArea.Content>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar className={styles.Scrollbar}>
<ScrollArea.Thumb className={styles.Thumb} />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
);
}
```
## Anatomy
Import the component and assemble its parts:
```jsx title="Anatomy"
import { ScrollArea } from "@base-ui/react/scroll-area";
<ScrollArea.Root>
<ScrollArea.Viewport>
<ScrollArea.Content>{/* Content */}</ScrollArea.Content>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar>
<ScrollArea.Thumb />
</ScrollArea.Scrollbar>
<ScrollArea.Corner />
</ScrollArea.Root>;
```
## Examples
### Both scrollbars
Use `<ScrollArea.Corner>` to prevent the scrollbars from intersecting.
#### Demo
##### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import { ScrollArea } from "@base-ui/react/scroll-area";
export default function ExampleScrollAreaBoth() {
return (
<ScrollArea.Root className="h-80 w-80 max-w-[calc(100vw-8rem)]">
<ScrollArea.Viewport className="h-full rounded-lg border border-gray-200 focus-visible:outline focus-visible:outline-blue-800 focus-visible:outline-offset-2">
<ScrollArea.Content className="p-5">
<ul className="m-0 grid list-none grid-cols-[repeat(10,6.25rem)] grid-rows-[repeat(10,6.25rem)] gap-3 p-0">
{Array.from({ length: 100 }, (_, i) => (
<li key={i} className="flex items-center justify-center rounded-lg bg-gray-100 text-sm font-medium text-gray-600">
{i + 1}
</li>
))}
</ul>
</ScrollArea.Content>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar className="relative flex rounded bg-gray-200 opacity-0 transition-opacity pointer-events-none before:absolute before:content-[''] data-[orientation=vertical]:m-2 data-[orientation=vertical]:w-1 data-[orientation=vertical]:before:h-full data-[orientation=vertical]:before:w-5 data-[orientation=vertical]:before:left-1/2 data-[orientation=vertical]:before:-translate-x-1/2 data-[orientation=horizontal]:m-2 data-[orientation=horizontal]:h-1 data-[orientation=horizontal]:before:h-5 data-[orientation=horizontal]:before:w-full data-[orientation=horizontal]:before:left-0 data-[orientation=horizontal]:before:right-0 data-[orientation=horizontal]:before:-bottom-2 data-[hovering]:pointer-events-auto data-[hovering]:opacity-100 data-[hovering]:delay-0 data-[scrolling]:pointer-events-auto data-[scrolling]:opacity-100 data-[scrolling]:duration-0">
<ScrollArea.Thumb className="w-full rounded bg-gray-500" />
</ScrollArea.Scrollbar>
<ScrollArea.Scrollbar className="relative flex rounded bg-gray-200 opacity-0 transition-opacity pointer-events-none before:absolute before:content-[''] data-[orientation=vertical]:m-2 data-[orientation=vertical]:w-1 data-[orientation=vertical]:before:h-full data-[orientation=vertical]:before:w-5 data-[orientation=vertical]:before:left-1/2 data-[orientation=vertical]:before:-translate-x-1/2 data-[orientation=horizontal]:m-2 data-[orientation=horizontal]:h-1 data-[orientation=horizontal]:before:h-5 data-[orientation=horizontal]:before:w-full data-[orientation=horizontal]:before:left-0 data-[orientation=horizontal]:before:right-0 data-[orientation=horizontal]:before:-bottom-2 data-[hovering]:pointer-events-auto data-[hovering]:opacity-100 data-[hovering]:delay-0 data-[scrolling]:pointer-events-auto data-[scrolling]:opacity-100 data-[scrolling]:duration-0" orientation="horizontal">
<ScrollArea.Thumb className="w-full rounded bg-gray-500" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner />
</ScrollArea.Root>
);
}
```
##### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.ScrollArea {
box-sizing: border-box;
width: 20rem;
height: 20rem;
max-width: calc(100vw - 8rem);
}
.Viewport {
height: 100%;
border-radius: 0.5rem;
outline: 1px solid var(--color-gray-200);
outline-offset: -1px;
&:focus-visible {
outline: 2px solid var(--color-blue);
}
}
.Content {
padding: 1.25rem;
}
.Grid {
display: grid;
grid-template-columns: repeat(10, 6.25rem);
grid-template-rows: repeat(10, 6.25rem);
gap: 0.75rem;
margin: 0;
padding: 0;
list-style: none;
}
.Item {
border-radius: 0.5rem;
background-color: var(--color-gray-100);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-gray-600);
}
.Scrollbar {
display: flex;
position: relative;
background-color: var(--color-gray-200);
border-radius: 0.375rem;
margin: 0.5rem;
opacity: 0;
transition: opacity 150ms;
pointer-events: none;
&::before {
content: "";
position: absolute;
}
&[data-scrolling] {
transition-duration: 0ms;
}
&[data-hovering],
&[data-scrolling] {
opacity: 1;
pointer-events: auto;
}
&[data-orientation="vertical"] {
width: 0.25rem;
margin: 0.5rem;
&::before {
width: 1.25rem;
height: 100%;
left: 50%;
transform: translateX(-50%);
}
}
&[data-orientation="horizontal"] {
height: 0.25rem;
margin: 0.5rem;
&::before {
width: 100%;
height: 1.25rem;
left: 0;
right: 0;
bottom: -0.5rem;
}
}
}
.Thumb {
width: 100%;
border-radius: inherit;
background-color: var(--color-gray-500);
}
```
```tsx
/* index.tsx */
import { ScrollArea } from "@base-ui/react/scroll-area";
import styles from "./index.module.css";
export default function ExampleScrollAreaBoth() {
return (
<ScrollArea.Root className={styles.ScrollArea}>
<ScrollArea.Viewport className={styles.Viewport}>
<ScrollArea.Content className={styles.Content}>
<ul className={styles.Grid}>
{Array.from({ length: 100 }, (_, i) => (
<li key={i} className={styles.Item}>
{i + 1}
</li>
))}
</ul>
</ScrollArea.Content>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar className={styles.Scrollbar}>
<ScrollArea.Thumb className={styles.Thumb} />
</ScrollArea.Scrollbar>
<ScrollArea.Scrollbar className={styles.Scrollbar} orientation="horizontal">
<ScrollArea.Thumb className={styles.Thumb} />
</ScrollArea.Scrollbar>
<ScrollArea.Corner />
</ScrollArea.Root>
);
}
```
### Gradient scroll fade
#### Demo
##### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import { ScrollArea } from "@base-ui/react/scroll-area";
export default function ExampleScrollAreaScrollFade() {
return (
<ScrollArea.Root className="box-border h-48 w-96 max-w-[calc(100vw-8rem)] rounded-lg bg-[var(--color-gray-50)]">
<ScrollArea.Viewport className="h-full rounded-md bg-[var(--color-gray-50)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-800 before:[--scroll-area-overflow-y-start:inherit] after:[--scroll-area-overflow-y-end:inherit] before:content-[''] after:content-[''] before:block after:block before:absolute after:absolute before:left-0 after:left-0 before:w-full after:w-full before:pointer-events-none after:pointer-events-none before:rounded-md after:rounded-md before:transition-[height] after:transition-[height] before:duration-100 after:duration-100 before:ease-out after:ease-out before:top-0 after:bottom-0 before:bg-[linear-gradient(to_bottom,var(--color-gray-50),transparent)] after:bg-[linear-gradient(to_top,var(--color-gray-50),transparent)] before:[height:min(40px,var(--scroll-area-overflow-y-start))] after:[height:min(40px,var(--scroll-area-overflow-y-end,40px))]">
<ScrollArea.Content className="flex flex-col gap-4 py-3 pr-6 pl-4 text-sm leading-[1.375rem] text-gray-900">
<p>Vernacular architecture is building done outside any academic tradition, and without professional guidance. It is not a particular architectural movement or style, but rather a broad category, encompassing a wide range and variety of building types, with differing methods of construction, from around the world, both historical and extant and classical and modern. Vernacular architecture constitutes 95% of the world's built environment, as estimated in 1995 by Amos Rapoport, as measured against the small percentage of new buildings every year designed by architects and built by engineers.</p>
<p>This type of architecture usually serves immediate, local needs, is constrained by the materials available in its particular region and reflects local traditions and cultural practices. The study of vernacular architecture does not examine formally schooled architects, but instead that of the design skills and tradition of local builders, who were rarely given any attribution for the work. More recently, vernacular architecture has been examined by designers and the building industry in an effort to be more energy conscious with contemporary design and construction—part of a broader interest in sustainable design.</p>
</ScrollArea.Content>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar className="pointer-events-none m-2 flex w-1 justify-center rounded bg-gray-200 opacity-0 transition-opacity duration-150 data-[hovering]:pointer-events-auto data-[hovering=true]:pointer-events-auto data-[hovering]:opacity-100 data-[hovering=true]:opacity-100 data-[scrolling]:pointer-events-auto data-[scrolling=true]:pointer-events-auto data-[scrolling]:opacity-100 data-[scrolling=true]:opacity-100 data-[scrolling]:duration-0 data-[scrolling=true]:duration-0 before:absolute before:h-full before:w-5 before:content-['']">
<ScrollArea.Thumb className="w-full rounded bg-gray-500" />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
);
}
```
##### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.ScrollArea {
box-sizing: border-box;
width: 24rem;
height: 12rem;
max-width: calc(100vw - 8rem);
background-color: var(--color-gray-50);
border-radius: 0.5rem;
}
.Viewport {
height: 100%;
border-radius: 0.375rem;
background: var(--color-gray-50);
&:focus-visible {
outline: 2px solid var(--color-blue);
}
&::before,
&::after {
content: "";
display: block;
left: 0;
width: 100%;
position: absolute;
pointer-events: none;
border-radius: 0.375rem;
transition: height 0.1s ease-out;
}
&::before {
--scroll-area-overflow-y-start: inherit;
top: 0;
height: min(40px, var(--scroll-area-overflow-y-start));
background: linear-gradient(to bottom, var(--color-gray-50), transparent);
}
&::after {
--scroll-area-overflow-y-end: inherit;
bottom: 0;
height: min(40px, var(--scroll-area-overflow-y-end, 40px));
background: linear-gradient(to top, var(--color-gray-50), transparent);
}
}
.Content {
display: flex;
flex-direction: column;
gap: 1rem;
padding-block: 0.75rem;
padding-left: 1rem;
padding-right: 1.5rem;
}
.Paragraph {
margin: 0;
font-size: 0.875rem;
line-height: 1.375rem;
color: var(--color-gray-900);
}
.Scrollbar {
display: flex;
justify-content: center;
background-color: var(--color-gray-200);
width: 0.25rem;
border-radius: 0.375rem;
margin: 0.5rem;
opacity: 0;
transition: opacity 150ms;
pointer-events: none;
&[data-scrolling] {
transition-duration: 0ms;
}
&[data-hovering],
&[data-scrolling] {
opacity: 1;
pointer-events: auto;
}
&::before {
content: "";
position: absolute;
width: 1.25rem;
height: 100%;
}
}
.Thumb {
width: 100%;
border-radius: inherit;
background-color: var(--color-gray-500);
}
.ScrollUp {
position: absolute;
top: 0;
z-index: 1;
opacity: 0;
transition: opacity 0.15s;
pointer-events: none;
[data-overflow-y-start] & {
opacity: 1;
pointer-events: auto;
}
}
```
```tsx
/* index.tsx */
import { ScrollArea } from "@base-ui/react/scroll-area";
import styles from "./index.module.css";
export default function ExampleScrollAreaScrollFade() {
return (
<ScrollArea.Root className={styles.ScrollArea}>
<ScrollArea.Viewport className={styles.Viewport}>
<ScrollArea.Content className={styles.Content}>
<p className={styles.Paragraph}>Vernacular architecture is building done outside any academic tradition, and without professional guidance. It is not a particular architectural movement or style, but rather a broad category, encompassing a wide range and variety of building types, with differing methods of construction, from around the world, both historical and extant and classical and modern. Vernacular architecture constitutes 95% of the world's built environment, as estimated in 1995 by Amos Rapoport, as measured against the small percentage of new buildings every year designed by architects and built by engineers.</p>
<p className={styles.Paragraph}>This type of architecture usually serves immediate, local needs, is constrained by the materials available in its particular region and reflects local traditions and cultural practices. The study of vernacular architecture does not examine formally schooled architects, but instead that of the design skills and tradition of local builders, who were rarely given any attribution for the work. More recently, vernacular architecture has been examined by designers and the building industry in an effort to be more energy conscious with contemporary design and construction—part of a broader interest in sustainable design.</p>
</ScrollArea.Content>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar className={styles.Scrollbar}>
<ScrollArea.Thumb className={styles.Thumb} />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
);
}
```
Use the viewport overflow CSS variables to fade the scroll edges, which gradually increases in strength as the user scrolls away from the edges.
The CSS variables do not inherit by default to improve rendering performance in complex scroll areas with deep subtrees. To use them in child elements (or pseudo-elements on `<ScrollArea.Viewport>`), you must manually set each variable to `inherit`.
```css title="scroll-area.module.css" {15,22}
.Viewport {
&::before,
&::after {
content: "";
display: block;
left: 0;
width: 100%;
position: absolute;
pointer-events: none;
border-radius: 0.375rem;
transition: height 0.1s ease-out;
}
&::before {
--scroll-area-overflow-y-start: inherit;
top: 0;
height: min(40px, var(--scroll-area-overflow-y-start));
background: linear-gradient(to bottom, var(--color-gray-50), transparent);
}
&::after {
--scroll-area-overflow-y-end: inherit;
bottom: 0;
height: min(40px, var(--scroll-area-overflow-y-end, 40px));
background: linear-gradient(to top, var(--color-gray-50), transparent);
}
}
```
For SSR, a fallback can be used as part of the `var()` function to provide a default height.
```css title="SSR fallback"
.Viewport::after {
height: min(40px, var(--scroll-area-overflow-y-end, 40px));
}
```
## API reference
### Root
Groups all parts of the scroll area.
Renders a `<div>` element.
**Root Props:**
| Prop | Type | Default | Description |
| :-------------------- | :----------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| overflowEdgeThreshold | `number \| Partial<{ xStart: number, xEnd: number, yStart: number, yEnd: number }>` | `0` | The threshold in pixels that must be passed before the overflow edge attributes are applied.
Accepts a single number for all edges or an object to configure them individually. |
| className | `string \| ((state: ScrollArea.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: ScrollArea.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: ScrollArea.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Root Data Attributes:**
| Attribute | Type | Description |
| :-------------------- | :--- | :---------------------------------------------------------------- |
| data-has-overflow-x | - | Present when the scroll area content is wider than the viewport. |
| data-has-overflow-y | - | Present when the scroll area content is taller than the viewport. |
| data-overflow-x-end | - | Present when there is overflow on the horizontal end side. |
| data-overflow-x-start | - | Present when there is overflow on the horizontal start side. |
| data-overflow-y-end | - | Present when there is overflow on the vertical end side. |
| data-overflow-y-start | - | Present when there is overflow on the vertical start side. |
**Root CSS Variables:**
| Variable | Type | Default | Description |
| :-------------------------- | :------- | :------ | :------------------------------- |
| --scroll-area-corner-height | `number` | - | The scroll area's corner height. |
| --scroll-area-corner-width | `number` | - | The scroll area's corner width. |
### Viewport
The actual scrollable container of the scroll area.
Renders a `<div>` element.
**Viewport Props:**
| Prop | Type | Default | Description |
| :-------- | :--------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: ScrollArea.Viewport.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: ScrollArea.Viewport.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: ScrollArea.Viewport.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Viewport Data Attributes:**
| Attribute | Type | Description |
| :-------------------- | :--- | :---------------------------------------------------------------- |
| data-has-overflow-x | - | Present when the scroll area content is wider than the viewport. |
| data-has-overflow-y | - | Present when the scroll area content is taller than the viewport. |
| data-overflow-x-end | - | Present when there is overflow on the horizontal end side. |
| data-overflow-x-start | - | Present when there is overflow on the horizontal start side. |
| data-overflow-y-end | - | Present when there is overflow on the vertical end side. |
| data-overflow-y-start | - | Present when there is overflow on the vertical start side. |
**Viewport CSS Variables:**
| Variable | Type | Default | Description |
| :----------------------------- | :------- | :------ | :----------------------------------------------------- |
| --scroll-area-overflow-x-end | `number` | - | The distance from the horizontal end edge in pixels. |
| --scroll-area-overflow-x-start | `number` | - | The distance from the horizontal start edge in pixels. |
| --scroll-area-overflow-y-end | `number` | - | The distance from the vertical end edge in pixels. |
| --scroll-area-overflow-y-start | `number` | - | The distance from the vertical start edge in pixels. |
### Content
A container for the content of the scroll area.
Renders a `<div>` element.
**Content Props:**
| Prop | Type | Default | Description |
| :-------- | :-------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: ScrollArea.Content.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: ScrollArea.Content.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: ScrollArea.Content.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Scrollbar
A vertical or horizontal scrollbar for the scroll area.
Renders a `<div>` element.
**Scrollbar Props:**
| Prop | Type | Default | Description |
| :---------- | :---------------------------------------------------------------------------------------- | :----------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| orientation | `'horizontal' \| 'vertical'` | `'vertical'` | Whether the scrollbar controls vertical or horizontal scroll. |
| className | `string \| ((state: ScrollArea.Scrollbar.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: ScrollArea.Scrollbar.State) => CSSProperties \| undefined)` | - | - |
| keepMounted | `boolean` | `false` | Whether to keep the HTML element in the DOM when the viewport isn’t scrollable. |
| render | `ReactElement \| ((props: HTMLProps, state: ScrollArea.Scrollbar.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Scrollbar Data Attributes:**
| Attribute | Type | Description |
| :-------------------- | :--------------------------- | :---------------------------------------------------------------- |
| data-orientation | `'horizontal' \| 'vertical'` | Indicates the orientation of the scrollbar. |
| data-has-overflow-x | - | Present when the scroll area content is wider than the viewport. |
| data-has-overflow-y | - | Present when the scroll area content is taller than the viewport. |
| data-hovering | - | Present when the pointer is over the scroll area. |
| data-overflow-x-end | - | Present when there is overflow on the horizontal end side. |
| data-overflow-x-start | - | Present when there is overflow on the horizontal start side. |
| data-overflow-y-end | - | Present when there is overflow on the vertical end side. |
| data-overflow-y-start | - | Present when there is overflow on the vertical start side. |
| data-scrolling | - | Present when the users scrolls inside the scroll area. |
**Scrollbar CSS Variables:**
| Variable | Type | Default | Description |
| :------------------------- | :------- | :------ | :------------------------------ |
| --scroll-area-thumb-height | `number` | - | The scroll area thumb's height. |
| --scroll-area-thumb-width | `number` | - | The scroll area thumb's width. |
### Thumb
The draggable part of the scrollbar that indicates the current scroll position.
Renders a `<div>` element.
**Thumb Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: ScrollArea.Thumb.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: ScrollArea.Thumb.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: ScrollArea.Thumb.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Thumb Data Attributes:**
| Attribute | Type | Description |
| :--------------- | :--------------------------- | :------------------------------------------ |
| data-orientation | `'horizontal' \| 'vertical'` | Indicates the orientation of the scrollbar. |
### Corner
A small rectangular area that appears at the intersection of horizontal and vertical scrollbars.
Renders a `<div>` element.
**Corner Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: ScrollArea.Corner.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: ScrollArea.Corner.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: ScrollArea.Corner.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
references/select.md
---
title: Select
subtitle: A common form component for choosing a predefined value in a dropdown menu.
description: A high-quality, unstyled React select component for choosing a predefined value in a dropdown menu.
---
# Select
A high-quality, unstyled React select component for choosing a predefined value in a dropdown menu.
## v1.3.0 notes
- `Select` adds a `Label` part for component-native labeling patterns.
- Prefer the built-in label part or an explicit `Field.Label` strategy instead of wiring ad hoc `aria-labelledby` relationships by hand.
## v1.4.0 notes
- Browser autofill with object values is more reliable when the browser autofills using the visible label.
- Touch reopen/highlight behavior and scroll-arrow edge behavior were fixed; re-check custom mobile patches before keeping them.
- `alignItemWithTrigger` now behaves better in RTL layouts.
## v1.9.0 notes
- Automatic unmounting is decoupled from `actionsRef`. Call `eventDetails.preventUnmountOnClose()` in `onOpenChange` to opt out, then end the closing phase with `actionsRef.current.unmount()`. See `popover.md`.
- Multiple values accept readonly arrays.
- Label focus works in Shadow DOM, and typeahead state resets when the select closes without moving focus.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import * as React from "react";
import { Select } from "@base-ui/react/select";
import { Field } from "@base-ui/react/field";
const apples = [
{ label: "Gala", value: "gala" },
{ label: "Fuji", value: "fuji" },
{ label: "Honeycrisp", value: "honeycrisp" },
{ label: "Granny Smith", value: "granny-smith" },
{ label: "Pink Lady", value: "pink-lady" },
];
export default function ExampleSelect() {
return (
<Field.Root className="flex flex-col gap-1">
<Field.Label className="cursor-default text-sm leading-5 font-medium text-gray-900" nativeLabel={false} render={<div />}>
Apple
</Field.Label>
<Select.Root items={apples}>
<Select.Trigger className="flex h-10 min-w-40 items-center justify-between gap-3 rounded-md border border-gray-200 pr-3 pl-3.5 text-base bg-[canvas] text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 data-[popup-open]:bg-gray-100">
<Select.Value className="data-[placeholder]:opacity-60" placeholder="Select apple" />
<Select.Icon className="flex">
<ChevronUpDownIcon />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Positioner className="outline-none select-none z-10" sideOffset={8}>
<Select.Popup className="group min-w-[var(--anchor-width)] origin-[var(--transform-origin)] bg-clip-padding rounded-md bg-[canvas] text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[side=none]:min-w-[calc(var(--anchor-width)+1rem)] data-[side=none]:data-[ending-style]:transition-none data-[starting-style]:scale-90 data-[starting-style]:opacity-0 data-[side=none]:data-[starting-style]:scale-100 data-[side=none]:data-[starting-style]:opacity-100 data-[side=none]:data-[starting-style]:transition-none dark:shadow-none dark:outline-gray-300">
<Select.ScrollUpArrow className="top-0 z-[1] flex h-4 w-full cursor-default items-center justify-center rounded-md bg-[canvas] text-center text-xs before:absolute data-[side=none]:before:top-[-100%] before:left-0 before:h-full before:w-full before:content-['']" />
<Select.List className="relative py-1 scroll-py-6 overflow-y-auto max-h-[var(--available-height)]">
{apples.map(({ label, value }) => (
<Select.Item key={label} value={value} className="grid cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-2 pr-4 pl-2.5 text-sm leading-4 outline-none select-none group-data-[side=none]:pr-12 group-data-[side=none]:text-base group-data-[side=none]:leading-4 data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 pointer-coarse:py-2.5 pointer-coarse:text-[0.925rem]">
<Select.ItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Select.ItemIndicator>
<Select.ItemText className="col-start-2">{label}</Select.ItemText>
</Select.Item>
))}
</Select.List>
<Select.ScrollDownArrow className="bottom-0 z-[1] flex h-4 w-full cursor-default items-center justify-center rounded-md bg-[canvas] text-center text-xs before:absolute before:left-0 before:h-full before:w-full before:content-[''] bottom-0 data-[side=none]:before:bottom-[-100%]" />
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
</Field.Root>
);
}
function ChevronUpDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" stroke="currentcolor" strokeWidth="1.5" {...props}>
<path d="M0.5 4.5L4 1.5L7.5 4.5" />
<path d="M0.5 7.5L4 10.5L7.5 7.5" />
</svg>
);
}
function CheckIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="10" height="10" viewBox="0 0 10 10" {...props}>
<path d="M9.1603 1.12218C9.50684 1.34873 9.60427 1.81354 9.37792 2.16038L5.13603 8.66012C5.01614 8.8438 4.82192 8.96576 4.60451 8.99384C4.3871 9.02194 4.1683 8.95335 4.00574 8.80615L1.24664 6.30769C0.939709 6.02975 0.916013 5.55541 1.19372 5.24822C1.47142 4.94102 1.94536 4.91731 2.2523 5.19524L4.36085 7.10461L8.12299 1.33999C8.34934 0.993152 8.81376 0.895638 9.1603 1.12218Z" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Field {
display: flex;
flex-direction: column;
align-items: start;
gap: 0.25rem;
}
.Label {
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
color: var(--color-gray-900);
cursor: default;
}
.Value[data-placeholder] {
opacity: 0.6;
}
.Select {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
height: 2.5rem;
padding-left: 0.875rem;
padding-right: 0.75rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: canvas;
font-family: inherit;
font-size: 1rem;
line-height: 1.5rem;
color: var(--color-gray-900);
-webkit-user-select: none;
user-select: none;
min-width: 10rem;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.SelectIcon {
display: flex;
}
.Positioner {
outline: none;
z-index: 1;
-webkit-user-select: none;
user-select: none;
}
.Popup {
box-sizing: border-box;
border-radius: 0.375rem;
background-color: canvas;
background-clip: padding-box;
color: var(--color-gray-900);
min-width: var(--anchor-width);
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
&[data-side="none"] {
transition: none;
transform: none;
opacity: 1;
min-width: calc(var(--anchor-width) + 1rem);
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
}
}
.List {
box-sizing: border-box;
position: relative;
padding-block: 0.25rem;
overflow-y: auto;
max-height: var(--available-height);
scroll-padding-block: 1.5rem;
}
.Arrow {
display: flex;
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.Item {
box-sizing: border-box;
outline: 0;
font-size: 0.875rem;
line-height: 1rem;
padding-block: 0.5rem;
padding-left: 0.625rem;
padding-right: 1rem;
display: grid;
gap: 0.5rem;
align-items: center;
grid-template-columns: 0.75rem 1fr;
cursor: default;
-webkit-user-select: none;
user-select: none;
@media (pointer: coarse) {
padding-block: 0.625rem;
font-size: 0.925rem;
}
[data-side="none"] & {
font-size: 1rem;
padding-right: 3rem;
}
&[data-highlighted] {
z-index: 0;
position: relative;
color: var(--color-gray-50);
}
&[data-highlighted]::before {
content: "";
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0.25rem;
border-radius: 0.25rem;
background-color: var(--color-gray-900);
}
}
.ItemIndicator {
grid-column-start: 1;
}
.ItemIndicatorIcon {
display: block;
width: 0.75rem;
height: 0.75rem;
}
.ItemText {
grid-column-start: 2;
}
.ScrollArrow {
width: 100%;
background: canvas;
z-index: 1;
text-align: center;
cursor: default;
border-radius: 0.375rem;
height: 1rem;
font-size: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
&::before {
content: "";
position: absolute;
width: 100%;
height: 100%;
left: 0;
}
&[data-direction="up"] {
&[data-side="none"] {
&::before {
top: -100%;
}
}
}
&[data-direction="down"] {
bottom: 0;
&[data-side="none"] {
&::before {
bottom: -100%;
}
}
}
}
```
```tsx
/* index.tsx */
import * as React from "react";
import { Select } from "@base-ui/react/select";
import { Field } from "@base-ui/react/field";
import styles from "./index.module.css";
const apples = [
{ label: "Gala", value: "gala" },
{ label: "Fuji", value: "fuji" },
{ label: "Honeycrisp", value: "honeycrisp" },
{ label: "Granny Smith", value: "granny-smith" },
{ label: "Pink Lady", value: "pink-lady" },
];
export default function ExampleSelect() {
return (
<Field.Root className={styles.Field}>
<Field.Label className={styles.Label} nativeLabel={false} render={<div />}>
Apple
</Field.Label>
<Select.Root items={apples}>
<Select.Trigger className={styles.Select}>
<Select.Value className={styles.Value} placeholder="Select apple" />
<Select.Icon className={styles.SelectIcon}>
<ChevronUpDownIcon />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Positioner className={styles.Positioner} sideOffset={8}>
<Select.Popup className={styles.Popup}>
<Select.ScrollUpArrow className={styles.ScrollArrow} />
<Select.List className={styles.List}>
{apples.map(({ label, value }) => (
<Select.Item key={label} value={value} className={styles.Item}>
<Select.ItemIndicator className={styles.ItemIndicator}>
<CheckIcon className={styles.ItemIndicatorIcon} />
</Select.ItemIndicator>
<Select.ItemText className={styles.ItemText}>{label}</Select.ItemText>
</Select.Item>
))}
</Select.List>
<Select.ScrollDownArrow className={styles.ScrollArrow} />
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
</Field.Root>
);
}
function ChevronUpDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" stroke="currentcolor" strokeWidth="1.5" {...props}>
<path d="M0.5 4.5L4 1.5L7.5 4.5" />
<path d="M0.5 7.5L4 10.5L7.5 7.5" />
</svg>
);
}
function CheckIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="10" height="10" viewBox="0 0 10 10" {...props}>
<path d="M9.1603 1.12218C9.50684 1.34873 9.60427 1.81354 9.37792 2.16038L5.13603 8.66012C5.01614 8.8438 4.82192 8.96576 4.60451 8.99384C4.3871 9.02194 4.1683 8.95335 4.00574 8.80615L1.24664 6.30769C0.939709 6.02975 0.916013 5.55541 1.19372 5.24822C1.47142 4.94102 1.94536 4.91731 2.2523 5.19524L4.36085 7.10461L8.12299 1.33999C8.34934 0.993152 8.81376 0.895638 9.1603 1.12218Z" />
</svg>
);
}
```
## Usage guidelines
- **Prefer Combobox for large lists**: Select is not filterable, aside from basic keyboard typeahead functionality to find items by focusing and highlighting them. Prefer [Combobox](/react/components/combobox.md) instead of Select when the number of items is sufficiently large to warrant filtering.
- **Special positioning behavior**: The select popup by default overlaps its trigger so the selected item's text is aligned with the trigger's value text. This behavior [can be disabled or customized](/react/components/select.md).
- **Form controls must have an accessible name**: It can be created using the `Field` component. See [Labeling a select](/react/components/select.md) and the [forms guide](/react/handbook/forms.md).
## Anatomy
Import the component and assemble its parts:
```jsx title="Anatomy"
import { Select } from "@base-ui/react/select";
<Select.Root>
<Select.Trigger>
<Select.Value />
<Select.Icon />
</Select.Trigger>
<Select.Portal>
<Select.Backdrop />
<Select.Positioner>
<Select.ScrollUpArrow />
<Select.Popup>
<Select.Arrow />
<Select.List>
<Select.Item>
<Select.ItemText />
<Select.ItemIndicator />
</Select.Item>
<Select.Separator />
<Select.Group>
<Select.GroupLabel />
</Select.Group>
</Select.List>
</Select.Popup>
<Select.ScrollDownArrow />
</Select.Positioner>
</Select.Portal>
</Select.Root>;
```
## Positioning
`<Select.Positioner>` has a special prop called `alignItemWithTrigger` which causes the positioning to act differently by default from other `Positioner` components.
The prop makes the select popup overlap the trigger so the selected item's text is aligned with the trigger's value text.
For styling, `data-side` is `"none"` on the `.Popup` and `.Positioner` parts when the mode is active.
To prevent the select popup from overlapping its trigger, set the `alignItemWithTrigger` prop to `false`.
When set to `true` (its default) there are a few important points to note about its behavior:
- **Interaction type dependent**: For UX reasons, the `alignItemWithTrigger` positioning mode is disabled if touch was the pointer type used to open the popup.
- **Viewport space dependent**: There must be enough space in the viewport to align the selected item's text with the trigger's value text without causing the popup to be too vertically small - otherwise, it falls back to the default positioning mode.
This can be customized by setting `min-height` on the `<Select.Positioner>` element; a smaller value will fallback less often.
Additionally, the trigger must be at least 20px from the edges of the top and bottom of the viewport, or it will also fall back.
- **Other positioning props are ignored**: Props like `side` or `align` have no effect unless the prop is set to `false` or when in fallback mode.
## Examples
### Typed wrapper component
The following example shows a typed wrapper around the Select component with correct type inference and type safety:
```tsx title="Specifying generic type parameters"
import * as React from "react";
import { Select } from "@base-ui/react/select";
export function MySelect<Value, Multiple extends boolean | undefined = false>(props: Select.Root.Props<Value, Multiple>): React.JSX.Element {
return <Select.Root {...props}>{/* ... */}</Select.Root>;
}
```
### Formatting the value
By default, the `<Select.Value>` component renders the raw `value`.
Passing the `items` prop to `<Select.Root>` instead renders the matching label for the rendered value:
```jsx title="items prop" "items"1,3
const items = [
{ value: null, label: "Select theme" },
{ value: "system", label: "System default" },
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
];
<Select.Root items={items}>
<Select.Value />
</Select.Root>;
```
A function can also be passed as the `children` prop of `<Select.Value>` to render a formatted value:
```jsx title="Lookup map" {8-12}
const items = {
monospace: 'Monospace',
serif: 'Serif',
'san-serif': 'Sans-serif',
};
<Select.Value>
{(value: keyof typeof items) => (
<span style={{ fontFamily: value }}>
{items[value]}
</span>
)}
</Select.Value>;
```
To avoid lookup, [object values](/react/components/select.md) for each item can also be used.
### Labeling a select
Use the [Field](/react/components/field.md) component to provide a visible label for the select trigger:
```tsx title="Using Field to label a select" {2,4}
<Field.Root>
<Field.Label nativeLabel={false} render={<div />}>
Theme
</Field.Label>
<Select.Root>{/* ... */}</Select.Root>
</Field.Root>
```
Replace the rendered `<label>` element with a `<div>` element and add `nativeLabel={false}` so it does not inherit native label behaviors. This ensures clicking on the label will focus the select trigger without opening the associated popup to match native `<select>` behavior, and prevents CSS `:hover` from activating on the trigger when hovering over the label.
### Placeholder values
To show a placeholder value, use the `placeholder` prop on `<Select.Value>`:
```jsx title="Placeholder item" {8}
const items = [
{ value: "system", label: "System default" },
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
];
<Select.Root items={items}>
<Select.Value placeholder="Select theme" />
</Select.Root>;
```
With placeholders, users cannot clear selected values using the select itself. If the select value should be clearable from the popup (instead of an external "reset" button), use a `null` item rendered in the list itself:
```jsx title="Clearable item" {2}
const items = [
{ value: null, label: "Select theme" },
{ value: "system", label: "System default" },
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
];
<Select.Root items={items}>
<Select.Value />
</Select.Root>;
```
### Multiple selection
Add the `multiple` prop to the `<Select.Root>` component to allow multiple selections.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Select } from "@base-ui/react/select";
import { Field } from "@base-ui/react/field";
const languages = {
javascript: "JavaScript",
typescript: "TypeScript",
python: "Python",
java: "Java",
csharp: "C#",
php: "PHP",
cpp: "C++",
rust: "Rust",
go: "Go",
swift: "Swift",
};
type Language = keyof typeof languages;
const values = Object.keys(languages) as Language[];
function renderValue(value: Language[]) {
if (value.length === 0) {
return "Select languages…";
}
const firstLanguage = languages[value[0]];
const additionalLanguages = value.length > 1 ? ` (+${value.length - 1} more)` : "";
return firstLanguage + additionalLanguages;
}
export default function MultiSelectExample() {
return (
<Field.Root className="flex flex-col gap-1">
<Field.Label className="cursor-default text-sm leading-5 font-medium text-gray-900" nativeLabel={false} render={<div />}>
Languages
</Field.Label>
<Select.Root multiple defaultValue={["javascript", "typescript"]}>
<Select.Trigger className="flex h-10 min-w-[14rem] items-center justify-between gap-3 rounded-md border border-gray-200 pr-3 pl-3.5 text-base bg-[canvas] text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 data-[popup-open]:bg-gray-100">
<Select.Value className="data-[placeholder]:opacity-60">{renderValue}</Select.Value>
<Select.Icon className="flex">
<ChevronUpDownIcon />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Positioner className="outline-none z-10" sideOffset={8} alignItemWithTrigger={false}>
<Select.Popup className="group max-h-[var(--available-height)] min-w-[var(--anchor-width)] origin-[var(--transform-origin)] bg-clip-padding overflow-y-auto rounded-md bg-[canvas] py-1 text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[side=none]:min-w-[calc(var(--anchor-width)+1rem)] data-[side=none]:data-[ending-style]:transition-none data-[starting-style]:scale-90 data-[starting-style]:opacity-0 data-[side=none]:data-[starting-style]:scale-100 data-[side=none]:data-[starting-style]:opacity-100 data-[side=none]:data-[starting-style]:transition-none dark:shadow-none dark:outline-gray-300">
{values.map((value) => (
<Select.Item key={value} value={value} className="grid cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-2 pr-4 pl-2.5 text-sm leading-4 outline-none select-none scroll-my-1 group-data-[side=none]:pr-12 group-data-[side=none]:text-base group-data-[side=none]:leading-4 pointer-coarse:py-2.5 pointer-coarse:text-[0.925rem] [@media(hover:hover)]:[&[data-highlighted]]:relative [@media(hover:hover)]:[&[data-highlighted]]:z-0 [@media(hover:hover)]:[&[data-highlighted]]:text-gray-50 [@media(hover:hover)]:[&[data-highlighted]]:before:content-[''] [@media(hover:hover)]:[&[data-highlighted]]:before:absolute [@media(hover:hover)]:[&[data-highlighted]]:before:inset-y-0 [@media(hover:hover)]:[&[data-highlighted]]:before:inset-x-1 [@media(hover:hover)]:[&[data-highlighted]]:before:rounded-sm [@media(hover:hover)]:[&[data-highlighted]]:before:bg-gray-900 [@media(hover:hover)]:[&[data-highlighted]]:before:z-[-1]">
<Select.ItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Select.ItemIndicator>
<Select.ItemText className="col-start-2">{languages[value]}</Select.ItemText>
</Select.Item>
))}
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
</Field.Root>
);
}
function ChevronUpDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" stroke="currentcolor" strokeWidth="1.5" {...props}>
<path d="M0.5 4.5L4 1.5L7.5 4.5" />
<path d="M0.5 7.5L4 10.5L7.5 7.5" />
</svg>
);
}
function CheckIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="10" height="10" viewBox="0 0 10 10" {...props}>
<path d="M9.1603 1.12218C9.50684 1.34873 9.60427 1.81354 9.37792 2.16038L5.13603 8.66012C5.01614 8.8438 4.82192 8.96576 4.60451 8.99384C4.3871 9.02194 4.1683 8.95335 4.00574 8.80615L1.24664 6.30769C0.939709 6.02975 0.916013 5.55541 1.19372 5.24822C1.47142 4.94102 1.94536 4.91731 2.2523 5.19524L4.36085 7.10461L8.12299 1.33999C8.34934 0.993152 8.81376 0.895638 9.1603 1.12218Z" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Field {
display: flex;
flex-direction: column;
align-items: start;
gap: 0.25rem;
}
.Label {
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
color: var(--color-gray-900);
cursor: default;
}
.Value[data-placeholder] {
opacity: 0.6;
}
.Select {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
height: 2.5rem;
padding-left: 0.875rem;
padding-right: 0.75rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: canvas;
font-family: inherit;
font-size: 1rem;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
min-width: 14rem;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.SelectIcon {
display: flex;
}
.Positioner {
outline: none;
z-index: 1;
}
.Popup {
box-sizing: border-box;
padding-block: 0.25rem;
border-radius: 0.375rem;
background-color: canvas;
background-clip: padding-box;
color: var(--color-gray-900);
min-width: var(--anchor-width);
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
overflow-y: auto;
max-height: var(--available-height);
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
&[data-side="none"] {
transition: none;
transform: none;
opacity: 1;
min-width: calc(var(--anchor-width) + 1rem);
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
}
}
.Item {
box-sizing: border-box;
outline: 0;
font-size: 0.875rem;
line-height: 1rem;
padding-block: 0.5rem;
padding-left: 0.625rem;
padding-right: 1rem;
display: grid;
gap: 0.5rem;
align-items: center;
grid-template-columns: 0.75rem 1fr;
cursor: default;
user-select: none;
scroll-margin-block: 0.25rem;
@media (pointer: coarse) {
padding-block: 0.625rem;
font-size: 0.925rem;
}
[data-side="none"] & {
font-size: 1rem;
padding-right: 3rem;
}
@media (hover: hover) {
&[data-highlighted] {
z-index: 0;
position: relative;
color: var(--color-gray-50);
}
&[data-highlighted]::before {
content: "";
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0.25rem;
border-radius: 0.25rem;
background-color: var(--color-gray-900);
}
}
}
.ItemIndicator {
grid-column-start: 1;
}
.ItemIndicatorIcon {
display: block;
width: 0.75rem;
height: 0.75rem;
}
.ItemText {
grid-column-start: 2;
}
.ScrollArrow {
width: 100%;
background: canvas;
z-index: 1;
text-align: center;
cursor: default;
border-radius: 0.375rem;
height: 1rem;
font-size: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
&::before {
content: "";
position: absolute;
width: 100%;
height: 100%;
left: 0;
}
&[data-direction="up"] {
top: 0;
&::before {
top: -100%;
}
}
&[data-direction="down"] {
bottom: 0;
&::before {
bottom: -100%;
}
}
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Select } from "@base-ui/react/select";
import { Field } from "@base-ui/react/field";
import styles from "./index.module.css";
const languages = {
javascript: "JavaScript",
typescript: "TypeScript",
python: "Python",
java: "Java",
csharp: "C#",
php: "PHP",
cpp: "C++",
rust: "Rust",
go: "Go",
swift: "Swift",
};
type Language = keyof typeof languages;
const values = Object.keys(languages) as Language[];
function renderValue(value: Language[]) {
if (value.length === 0) {
return "Select languages…";
}
const firstLanguage = languages[value[0]];
const additionalLanguages = value.length > 1 ? ` (+${value.length - 1} more)` : "";
return firstLanguage + additionalLanguages;
}
export default function MultiSelectExample() {
return (
<Field.Root className={styles.Field}>
<Field.Label className={styles.Label} nativeLabel={false} render={<div />}>
Languages
</Field.Label>
<Select.Root multiple defaultValue={["javascript", "typescript"]}>
<Select.Trigger className={styles.Select}>
<Select.Value className={styles.Value}>{renderValue}</Select.Value>
<Select.Icon className={styles.SelectIcon}>
<ChevronUpDownIcon />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Positioner className={styles.Positioner} sideOffset={8} alignItemWithTrigger={false}>
<Select.Popup className={styles.Popup}>
{values.map((value) => (
<Select.Item key={value} value={value} className={styles.Item}>
<Select.ItemIndicator className={styles.ItemIndicator}>
<CheckIcon className={styles.ItemIndicatorIcon} />
</Select.ItemIndicator>
<Select.ItemText className={styles.ItemText}>{languages[value]}</Select.ItemText>
</Select.Item>
))}
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
</Field.Root>
);
}
function ChevronUpDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" stroke="currentcolor" strokeWidth="1.5" {...props}>
<path d="M0.5 4.5L4 1.5L7.5 4.5" />
<path d="M0.5 7.5L4 10.5L7.5 7.5" />
</svg>
);
}
function CheckIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="10" height="10" viewBox="0 0 10 10" {...props}>
<path d="M9.1603 1.12218C9.50684 1.34873 9.60427 1.81354 9.37792 2.16038L5.13603 8.66012C5.01614 8.8438 4.82192 8.96576 4.60451 8.99384C4.3871 9.02194 4.1683 8.95335 4.00574 8.80615L1.24664 6.30769C0.939709 6.02975 0.916013 5.55541 1.19372 5.24822C1.47142 4.94102 1.94536 4.91731 2.2523 5.19524L4.36085 7.10461L8.12299 1.33999C8.34934 0.993152 8.81376 0.895638 9.1603 1.12218Z" />
</svg>
);
}
```
### Object values
Select items can use objects as values instead of primitives.
This lets you access the full object in custom render functions, and can avoid needing to specify `items` for lookup.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Select } from "@base-ui/react/select";
import { Field } from "@base-ui/react/field";
export default function ObjectValueSelect() {
return (
<Field.Root className="flex flex-col gap-1">
<Field.Label className="cursor-default text-sm leading-5 font-medium text-gray-900" nativeLabel={false} render={<div />}>
Shipping method
</Field.Label>
<Select.Root defaultValue={shippingMethods[0]} itemToStringValue={(item) => item.id}>
<Select.Trigger className="flex min-h-10 min-w-[16rem] items-start justify-between gap-3 rounded-md border border-gray-200 pr-3 pl-3.5 py-2 text-base bg-[canvas] text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 data-[popup-open]:bg-gray-100">
<Select.Value>
{(method: ShippingMethod) => (
<span className="flex flex-col items-start gap-0.5">
<span className="text-base leading-6">{method.name}</span>
<span className="text-xs leading-4 text-gray-600">
{method.duration} ({method.price})
</span>
</span>
)}
</Select.Value>
<Select.Icon className="flex items-center self-center">
<ChevronUpDownIcon />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Positioner className="outline-none select-none z-10" sideOffset={8}>
<Select.Popup className="group min-w-[var(--anchor-width)] origin-[var(--transform-origin)] bg-clip-padding rounded-md bg-[canvas] text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[side=none]:min-w-[calc(var(--anchor-width)+1rem)] data-[side=none]:data-[ending-style]:transition-none data-[starting-style]:scale-90 data-[starting-style]:opacity-0 data-[side=none]:data-[starting-style]:scale-100 data-[side=none]:data-[starting-style]:opacity-100 data-[side=none]:data-[starting-style] :transition-none dark:shadow-none dark:outline-gray-300">
<Select.ScrollUpArrow className="top-0 z-[1] flex h-4 w-full cursor-default items-center justify-center rounded-md bg-[canvas] text-center text-xs before:absolute data-[side=none]:before:top-[-100%] before:left-0 before:h-full before:w-full before:content-['']" />
<Select.List className="relative py-1 scroll-py-6 overflow-y-auto max-h-[var(--available-height)]">
{shippingMethods.map((method) => (
<Select.Item key={method.id} value={method} className="grid cursor-default grid-cols-[0.75rem_1fr] items-start gap-2 py-2.5 pr-4 pl-2.5 text-sm leading-4 outline-none select-none group-data-[side=none]:pr-12 group-data-[side=none]:text-base group-data-[side=none]:leading-5 pointer-coarse:py-2.5 pointer-coarse:text-[0.925rem] [@media(hover:hover)]:[&[data-highlighted]]:relative [@media(hover:hover)]:[&[data-highlighted]]:z-0 [@media(hover:hover)]:[&[data-highlighted]]:text-gray-50 [@media(hover:hover)]:[&[data-highlighted]]:before:content-[''] [@media(hover:hover)]:[&[data-highlighted]]:before:absolute [@media(hover:hover)]:[&[data-highlighted]]:before:inset-y-0 [@media(hover:hover)]:[&[data-highlighted]]:before:inset-x-1 [@media(hover:hover)]:[&[data-highlighted]]:before:rounded-sm [@media(hover:hover)]:[&[data-highlighted]]:before:bg-gray-900 [@media(hover:hover)]:[&[data-highlighted]]:before:z-[-1]">
<Select.ItemIndicator className="col-start-1 flex items-center self-start relative top-[0.4em]">
<CheckIcon className="size-3" />
</Select.ItemIndicator>
<Select.ItemText className="col-start-2 flex flex-col items-start gap-0.5">
<span className="text-base leading-6">{method.name}</span>
<span className="text-xs leading-4 opacity-80">
{method.duration} ({method.price})
</span>
</Select.ItemText>
</Select.Item>
))}
</Select.List>
<Select.ScrollDownArrow className="bottom-0 z-[1] flex h-4 w-full cursor-default items-center justify-center rounded-md bg-[canvas] text-center text-xs before:absolute before:left-0 before:h-full before:w-full before:content-[''] bottom-0 data-[side=none]:before:bottom-[-100%]" />
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
</Field.Root>
);
}
function ChevronUpDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" stroke="currentcolor" strokeWidth="1.5" {...props}>
<path d="M0.5 4.5L4 1.5L7.5 4.5" />
<path d="M0.5 7.5L4 10.5L7.5 7.5" />
</svg>
);
}
function CheckIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="10" height="10" viewBox="0 0 10 10" {...props}>
<path d="M9.1603 1.12218C9.50684 1.34873 9.60427 1.81354 9.37792 2.16038L5.13603 8.66012C5.01614 8.8438 4.82192 8.96576 4.60451 8.99384C4.3871 9.02194 4.1683 8.95335 4.00574 8.80615L1.24664 6.30769C0.939709 6.02975 0.916013 5.55541 1.19372 5.24822C1.47142 4.94102 1.94536 4.91731 2.2523 5.19524L4.36085 7.10461L8.12299 1.33999C8.34934 0.993152 8.81376 0.895638 9.1603 1.12218Z" />
</svg>
);
}
interface ShippingMethod {
id: string;
name: string;
duration: string;
price: string;
}
const shippingMethods: ShippingMethod[] = [
{
id: "standard",
name: "Standard",
duration: "Delivers in 4-6 business days",
price: "$4.99",
},
{
id: "express",
name: "Express",
duration: "Delivers in 2-3 business days",
price: "$9.99",
},
{
id: "overnight",
name: "Overnight",
duration: "Delivers next business day",
price: "$19.99",
},
];
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Field {
display: flex;
flex-direction: column;
align-items: start;
gap: 0.25rem;
}
.Label {
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
color: var(--color-gray-900);
cursor: default;
}
.Select {
box-sizing: border-box;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.75rem;
min-height: 2.5rem;
padding-block: 0.5rem;
padding-left: 0.875rem;
padding-right: 0.75rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: canvas;
font-family: inherit;
font-size: 1rem;
line-height: 1.5rem;
color: var(--color-gray-900);
-webkit-user-select: none;
user-select: none;
min-width: 16rem;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.ValueText {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.125rem;
}
.ValuePrimary {
font-size: 1rem;
line-height: 1.5rem;
}
.ValueSecondary {
font-size: 0.825rem;
line-height: 1rem;
color: var(--color-gray-600);
}
.SelectIcon {
display: flex;
align-items: center;
align-self: center;
}
.Positioner {
outline: none;
z-index: 1;
-webkit-user-select: none;
user-select: none;
}
.Popup {
box-sizing: border-box;
border-radius: 0.375rem;
background-color: canvas;
background-clip: padding-box;
color: var(--color-gray-900);
min-width: var(--anchor-width);
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
&[data-side="none"] {
transition: none;
transform: none;
opacity: 1;
min-width: calc(var(--anchor-width) + 1rem);
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
}
}
.List {
box-sizing: border-box;
position: relative;
padding-block: 0.25rem;
overflow-y: auto;
max-height: var(--available-height);
scroll-padding-block: 1.5rem;
}
.Item {
box-sizing: border-box;
outline: 0;
font-size: 0.875rem;
line-height: 1rem;
padding-block: 0.5rem;
padding-left: 0.625rem;
padding-right: 1rem;
display: grid;
gap: 0.5rem;
align-items: flex-start;
grid-template-columns: 0.75rem 1fr;
cursor: default;
-webkit-user-select: none;
user-select: none;
@media (pointer: coarse) {
padding-block: 0.625rem;
font-size: 0.925rem;
}
[data-side="none"] & {
font-size: 1rem;
padding-right: 3rem;
}
&[data-highlighted] {
z-index: 0;
position: relative;
color: var(--color-gray-50);
.ItemDescription {
color: currentColor;
}
}
&[data-highlighted]::before {
content: "";
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0.25rem;
border-radius: 0.25rem;
background-color: var(--color-gray-900);
}
}
.ItemIndicator {
grid-column-start: 1;
display: flex;
align-items: center;
justify-content: center;
align-self: start;
position: relative;
top: 0.4em;
}
.ItemIndicatorIcon {
display: block;
width: 0.75rem;
height: 0.75rem;
}
.ItemText {
grid-column-start: 2;
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.ItemLabel {
font-size: 1rem;
line-height: 1.5rem;
}
.ItemDescription {
font-size: 0.825rem;
line-height: 1rem;
opacity: 0.8;
}
.ScrollArrow {
width: 100%;
background: canvas;
z-index: 1;
text-align: center;
cursor: default;
border-radius: 0.375rem;
height: 1rem;
font-size: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
&::before {
content: "";
position: absolute;
width: 100%;
height: 100%;
left: 0;
}
&[data-direction="up"] {
&[data-side="none"] {
&::before {
top: -100%;
}
}
}
&[data-direction="down"] {
bottom: 0;
&[data-side="none"] {
&::before {
bottom: -100%;
}
}
}
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Select } from "@base-ui/react/select";
import { Field } from "@base-ui/react/field";
import styles from "./index.module.css";
export default function ObjectValueSelect() {
return (
<Field.Root className={styles.Field}>
<Field.Label className={styles.Label} nativeLabel={false} render={<div />}>
Shipping method
</Field.Label>
<Select.Root defaultValue={shippingMethods[0]} itemToStringValue={(item) => item.id}>
<Select.Trigger className={styles.Select}>
<Select.Value>
{(method: ShippingMethod) => (
<span className={styles.ValueText}>
<span className={styles.ValuePrimary}>{method.name}</span>
<span className={styles.ValueSecondary}>
{method.duration} ({method.price})
</span>
</span>
)}
</Select.Value>
<Select.Icon className={styles.SelectIcon}>
<ChevronUpDownIcon />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Positioner className={styles.Positioner} sideOffset={8}>
<Select.Popup className={styles.Popup}>
<Select.ScrollUpArrow className={styles.ScrollArrow} />
<Select.List className={styles.List}>
{shippingMethods.map((method) => (
<Select.Item key={method.id} value={method} className={styles.Item}>
<Select.ItemIndicator className={styles.ItemIndicator}>
<CheckIcon className={styles.ItemIndicatorIcon} />
</Select.ItemIndicator>
<Select.ItemText className={styles.ItemText}>
<span className={styles.ItemLabel}>{method.name}</span>
<span className={styles.ItemDescription}>
{method.duration} ({method.price})
</span>
</Select.ItemText>
</Select.Item>
))}
</Select.List>
<Select.ScrollDownArrow className={styles.ScrollArrow} />
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
</Field.Root>
);
}
function ChevronUpDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" stroke="currentcolor" strokeWidth="1.5" {...props}>
<path d="M0.5 4.5L4 1.5L7.5 4.5" />
<path d="M0.5 7.5L4 10.5L7.5 7.5" />
</svg>
);
}
function CheckIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="10" height="10" viewBox="0 0 10 10" {...props}>
<path d="M9.1603 1.12218C9.50684 1.34873 9.60427 1.81354 9.37792 2.16038L5.13603 8.66012C5.01614 8.8438 4.82192 8.96576 4.60451 8.99384C4.3871 9.02194 4.1683 8.95335 4.00574 8.80615L1.24664 6.30769C0.939709 6.02975 0.916013 5.55541 1.19372 5.24822C1.47142 4.94102 1.94536 4.91731 2.2523 5.19524L4.36085 7.10461L8.12299 1.33999C8.34934 0.993152 8.81376 0.895638 9.1603 1.12218Z" />
</svg>
);
}
interface ShippingMethod {
id: string;
name: string;
duration: string;
price: string;
}
const shippingMethods: ShippingMethod[] = [
{
id: "standard",
name: "Standard",
duration: "Delivers in 4-6 business days",
price: "$4.99",
},
{
id: "express",
name: "Express",
duration: "Delivers in 2-3 business days",
price: "$9.99",
},
{
id: "overnight",
name: "Overnight",
duration: "Delivers next business day",
price: "$19.99",
},
];
```
## API reference
### Root
Groups all parts of the select.
Doesn’t render its own HTML element.
**Root Props:**
| Prop | Type | Default | Description |
| :--------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | `string` | - | Identifies the field when a form is submitted. |
| defaultValue | `Value[] \| Value \| null` | - | The uncontrolled value of the select when it’s initially rendered.To render a controlled select, use the `value` prop instead. |
| value | `Value[] \| Value \| null` | - | The value of the select. Use when controlled. |
| onValueChange | `((value: Value[] \| Value \| any \| null, eventDetails: Select.Root.ChangeEventDetails) => void)` | - | Event handler called when the value of the select changes. |
| defaultOpen | `boolean` | `false` | Whether the select popup is initially open.To render a controlled select popup, use the `open` prop instead. |
| open | `boolean` | - | Whether the select popup is currently open. |
| onOpenChange | `((open: boolean, eventDetails: Select.Root.ChangeEventDetails) => void)` | - | Event handler called when the select popup is opened or closed. |
| highlightItemOnHover | `boolean` | `true` | Whether moving the pointer over items should highlight them.
Disabling this prop allows CSS `:hover` to be differentiated from the `:focus` (`data-highlighted`) state. |
| actionsRef | `RefObject<Select.Root.Actions \| null>` | - | A ref to imperative actions.\* `unmount`: Ends the closing phase of the select after an externally controlled closing animation finishes.
Call `preventUnmountOnClose()` in `onOpenChange` first, otherwise the select completes closing on its own.
Whether it leaves the DOM is decided by `keepMounted` on the portal. |
| \* `close`: Closes the select imperatively when called. |
| isItemEqualToValue | `((itemValue: Value, value: Value) => boolean)` | - | Custom comparison logic used to determine if a select item value matches the current selected value. Useful when item values are objects without matching referentially.
Defaults to `Object.is` comparison. |
| itemToStringLabel | `((itemValue: Value) => string)` | - | When the item values are objects (`<Select.Item value={object}>`), this function converts the object value to a string representation for display in the trigger.
If the shape of the object is `{ value, label }`, the label will be used automatically without needing to specify this prop. |
| itemToStringValue | `((itemValue: Value) => string)` | - | When the item values are objects (`<Select.Item value={object}>`), this function converts the object value to a string representation for form submission.
If the shape of the object is `{ value, label }`, the value will be used automatically without needing to specify this prop. |
| items | `Record<string, ReactNode> \| ({ label: ReactNode, value: any })[]` | - | Data structure of the items rendered in the select popup.
When specified, `<Select.Value>` renders the label of the selected item instead of the raw value. |
| modal | `boolean` | `true` | Determines if the select enters a modal state when open.\* `true`: user interaction is limited to the select: document page scroll is locked and pointer interactions on outside elements are disabled. |
| \* `false`: user interaction with the rest of the document is allowed. |
| multiple | `boolean \| undefined` | `false` | Whether multiple items can be selected. |
| onOpenChangeComplete | `((open: boolean) => void)` | - | Event handler called after any animations complete when the select popup is opened or closed. |
| disabled | `boolean` | `false` | Whether the component should ignore user interaction. |
| readOnly | `boolean` | `false` | Whether the user should be unable to choose a different option from the select popup. |
| required | `boolean` | `false` | Whether the user must choose a value before submitting a form. |
| inputRef | `Ref<HTMLInputElement>` | - | A ref to access the hidden input element. |
| id | `string` | - | The id of the Select. |
| children | `ReactNode` | - | - |
### Trigger
A button that opens the select popup.
Renders a `<button>` element.
**Trigger Props:**
| Prop | Type | Default | Description |
| :----------- | :---------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| nativeButton | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it
via the `render` prop.
Set to `false` if the rendered element is not a button (e.g. `<div>`). |
| disabled | `boolean` | - | Whether the component should ignore user interaction. |
| children | `ReactNode` | - | - |
| className | `string \| ((state: Select.Trigger.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Select.Trigger.State) => CSSProperties \| undefined)` | - | \* |
| render | `ReactElement \| ((props: HTMLProps, state: Select.Trigger.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Trigger Data Attributes:**
| Attribute | Type | Description |
| :--------------- | :--- | :------------------------------------------------------------------------ |
| data-popup-open | - | Present when the corresponding select is open. |
| data-pressed | - | Present when the trigger is pressed. |
| data-disabled | - | Present when the select is disabled. |
| data-readonly | - | Present when the select is readonly. |
| data-required | - | Present when the select is required. |
| data-valid | - | Present when the select is in valid state (when wrapped in Field.Root). |
| data-invalid | - | Present when the select is in invalid state (when wrapped in Field.Root). |
| data-dirty | - | Present when the select's value has changed (when wrapped in Field.Root). |
| data-touched | - | Present when the select has been touched (when wrapped in Field.Root). |
| data-filled | - | Present when the select has a value (when wrapped in Field.Root). |
| data-focused | - | Present when the select trigger is focused (when wrapped in Field.Root). |
| data-placeholder | - | Present when the select doesn't have a value. |
### Value
A text label of the currently selected item.
Renders a `<span>` element.
**Value Props:**
| Prop | Type | Default | Description |
| :---------- | :-------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| placeholder | `ReactNode` | - | The placeholder value to display when no value is selected.
This is overridden by `children` if specified, or by a null item's label in `items`. |
| children | `ReactNode \| ((value: any) => ReactNode)` | - | Accepts a function that returns a `ReactNode` to format the selected value. |
| className | `string \| ((state: Select.Value.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Select.Value.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Select.Value.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Value Data Attributes:**
| Attribute | Type | Description |
| :--------------- | :--- | :-------------------------------------------- |
| data-placeholder | - | Present when the select doesn't have a value. |
### Icon
An icon that indicates that the trigger button opens a select popup.
Renders a `<span>` element.
**Icon Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Select.Icon.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Select.Icon.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Select.Icon.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Icon Data Attributes:**
| Attribute | Type | Description |
| :-------------- | :--- | :-------------------------------------------- |
| data-popup-open | - | Present when the corresponding popup is open. |
### Backdrop
An overlay displayed beneath the menu popup.
Renders a `<div>` element.
**Backdrop Props:**
| Prop | Type | Default | Description |
| :-------- | :----------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Select.Backdrop.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Select.Backdrop.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Select.Backdrop.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Backdrop Data Attributes:**
| Attribute | Type | Description |
| :------------------ | :--- | :---------------------------------------- |
| data-open | - | Present when the select is open. |
| data-closed | - | Present when the select is closed. |
| data-starting-style | - | Present when the select is animating in. |
| data-ending-style | - | Present when the select is animating out. |
### Portal
A portal element that moves the popup to a different part of the DOM.
By default, the portal element is appended to `<body>`.
Renders a `<div>` element.
**Portal Props:**
| Prop | Type | Default | Description |
| :-------- | :---------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| container | `HTMLElement \| ShadowRoot \| RefObject<HTMLElement \| ShadowRoot \| null> \| null` | - | A parent element to render the portal element into. |
| className | `string \| ((state: Select.Portal.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Select.Portal.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Select.Portal.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Positioner
Positions the select popup.
Renders a `<div>` element.
**Positioner Props:**
| Prop | Type | Default | Description |
| :-------------------- | :------------------------- | :--------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| alignItemWithTrigger | `boolean` | `true` | Whether the positioner overlaps the trigger so the selected item's text is aligned with the trigger's value text. This only applies to mouse input and is automatically disabled if there is not enough space. |
| disableAnchorTracking | `boolean` | `false` | Whether to disable the popup from tracking any layout shift of its positioning anchor. |
| align | `Align` | `'center'` | How to align the popup relative to the specified side. |
| alignOffset | `number \| OffsetFunction` | `0` | Additional offset along the alignment axis in pixels.
Also accepts a function that returns the offset to read the dimensions of the anchor
and positioner elements, along with its side and alignment.The function takes a `data` object parameter with the following properties:\* `data.anchor`: the dimensions of the anchor element with properties `width` and `height`. |
- `data.positioner`: the dimensions of the positioner element with properties `width` and `height`.
- `data.side`: which side of the anchor element the positioner is aligned against.
- `data.align`: how the positioner is aligned relative to the specified side. |
| side | `Side` | `'bottom'` | Which side of the anchor element to align the popup against.
May automatically change to avoid collisions. |
| sideOffset | `number \| OffsetFunction` | `0` | Distance between the anchor and the popup in pixels.
Also accepts a function that returns the distance to read the dimensions of the anchor
and positioner elements, along with its side and alignment.The function takes a `data` object parameter with the following properties:\* `data.anchor`: the dimensions of the anchor element with properties `width` and `height`.
- `data.positioner`: the dimensions of the positioner element with properties `width` and `height`.
- `data.side`: which side of the anchor element the positioner is aligned against.
- `data.align`: how the positioner is aligned relative to the specified side. |
| arrowPadding | `number` | `5` | Minimum distance to maintain between the arrow and the edges of the popup.Use it to prevent the arrow element from hanging out of the rounded corners of a popup. |
| anchor | `Element \| RefObject<Element \| null> \| VirtualElement \| (() => Element \| VirtualElement \| null) \| null` | - | An element to position the popup against.
By default, the popup will be positioned against the trigger. |
| collisionAvoidance | `CollisionAvoidance` | - | Determines how to handle collisions when positioning the popup. |
| collisionBoundary | `Boundary` | `'clipping-ancestors'` | An element or a rectangle that delimits the area that the popup is confined to. |
| collisionPadding | `Padding` | `5` | Additional space to maintain from the edge of the collision boundary. |
| sticky | `boolean` | `false` | Whether to maintain the popup in the viewport after
the anchor element was scrolled out of view. |
| positionMethod | `'fixed' \| 'absolute'` | `'absolute'` | Determines which CSS `position` property to use. |
| className | `string \| ((state: Select.Positioner.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Select.Positioner.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Select.Positioner.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Positioner Data Attributes:**
| Attribute | Type | Description |
| :----------------- | :----------------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| data-open | - | Present when the select popup is open. |
| data-closed | - | Present when the select popup is closed. |
| data-anchor-hidden | - | Present when the anchor is hidden. |
| data-align | `'start' \| 'center' \| 'end'` | Indicates how the popup is aligned relative to specified side. |
| data-side | `'none' \| 'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the popup is positioned relative to the trigger. |
**Positioner CSS Variables:**
| Variable | Type | Default | Description |
| :----------------- | :------- | :------ | :------------------------------------------------------------------------------------- |
| --anchor-height | `number` | - | The anchor's height. |
| --anchor-width | `number` | - | The anchor's width. |
| --available-height | `number` | - | The available height between the trigger and the edge of the viewport. |
| --available-width | `number` | - | The available width between the trigger and the edge of the viewport. |
| --transform-origin | `string` | - | The coordinates that this element is anchored to. Used for animations and transitions. |
### Popup
A container for the select list.
Renders a `<div>` element.
**Popup Props:**
| Prop | Type | Default | Description |
| :-------- | :-------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| children | `ReactNode` | - | - |
| className | `string \| ((state: Select.Popup.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Select.Popup.State) => CSSProperties \| undefined)` | - | \* |
| render | `ReactElement \| ((props: HTMLProps, state: Select.Popup.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Popup Data Attributes:**
| Attribute | Type | Description |
| :------------------ | :----------------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| data-open | - | Present when the select is open. |
| data-closed | - | Present when the select is closed. |
| data-align | `'start' \| 'center' \| 'end'` | Indicates how the popup is aligned relative to specified side. |
| data-side | `'none' \| 'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the popup is positioned relative to the trigger. |
| data-starting-style | - | Present when the select is animating in. |
| data-ending-style | - | Present when the select is animating out. |
### List
A container for the select items.
Renders a `<div>` element.
**List Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Select.List.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Select.List.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Select.List.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Arrow
Displays an element positioned against the select popup anchor.
Renders a `<div>` element.
**Arrow Props:**
| Prop | Type | Default | Description |
| :-------- | :-------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Select.Arrow.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Select.Arrow.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Select.Arrow.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Arrow Data Attributes:**
| Attribute | Type | Description |
| :-------------- | :----------------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| data-open | - | Present when the select popup is open. |
| data-closed | - | Present when the select popup is closed. |
| data-uncentered | - | Present when the select arrow is uncentered. |
| data-align | `'start' \| 'center' \| 'end'` | Indicates how the popup is aligned relative to specified side. |
| data-side | `'none' \| 'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the popup is positioned relative to the trigger. |
### Item
An individual option in the select popup.
Renders a `<div>` element.
**Item Props:**
| Prop | Type | Default | Description |
| :----------- | :------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| label | `string` | - | Specifies the text label to use when the item is matched during keyboard text navigation.Defaults to the item text content if not provided. |
| value | `any` | `null` | A unique value that identifies this select item. |
| nativeButton | `boolean` | `false` | Whether the component renders a native `<button>` element when replacing it
via the `render` prop.
Set to `true` if the rendered element is a native button. |
| disabled | `boolean` | `false` | Whether the component should ignore user interaction. |
| children | `ReactNode` | - | - |
| className | `string \| ((state: Select.Item.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Select.Item.State) => CSSProperties \| undefined)` | - | \* |
| render | `ReactElement \| ((props: HTMLProps, state: Select.Item.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Item Data Attributes:**
| Attribute | Type | Description |
| :--------------- | :--- | :------------------------------------------- |
| data-selected | - | Present when the select item is selected. |
| data-highlighted | - | Present when the select item is highlighted. |
| data-disabled | - | Present when the select item is disabled. |
### ItemText
A text label of the select item.
Renders a `<div>` element.
**ItemText Props:**
| Prop | Type | Default | Description |
| :-------- | :----------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Select.ItemText.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Select.ItemText.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Select.ItemText.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### ItemIndicator
Indicates whether the select item is selected.
Renders a `<span>` element.
**ItemIndicator Props:**
| Prop | Type | Default | Description |
| :---------- | :---------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| children | `ReactNode` | - | - |
| className | `string \| ((state: Select.ItemIndicator.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Select.ItemIndicator.State) => CSSProperties \| undefined)` | - | \* |
| keepMounted | `boolean` | - | Whether to keep the HTML element in the DOM when the item is not selected. |
| render | `ReactElement \| ((props: HTMLProps, state: Select.ItemIndicator.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Group
Groups related select items with the corresponding label.
Renders a `<div>` element.
**Group Props:**
| Prop | Type | Default | Description |
| :-------- | :-------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Select.Group.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Select.Group.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Select.Group.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### GroupLabel
An accessible label that is automatically associated with its parent group.
Renders a `<div>` element.
**GroupLabel Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Select.GroupLabel.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Select.GroupLabel.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Select.GroupLabel.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### ScrollUpArrow
An element that scrolls the select popup up when hovered. Does not render when using touch input.
Renders a `<div>` element.
**ScrollUpArrow Props:**
| Prop | Type | Default | Description |
| :---------- | :---------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Select.ScrollUpArrow.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Select.ScrollUpArrow.State) => CSSProperties \| undefined)` | - | - |
| keepMounted | `boolean` | `false` | Whether to keep the HTML element in the DOM while the select popup is not scrollable. |
| render | `ReactElement \| ((props: HTMLProps, state: Select.ScrollUpArrow.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**ScrollUpArrow Data Attributes:**
| Attribute | Type | Description |
| :------------------ | :----------------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| data-direction | `'up'` | Indicates the direction of the scroll arrow. |
| data-side | `'none' \| 'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the popup is positioned relative to the trigger. |
| data-visible | - | Present when the scroll arrow is visible. |
| data-starting-style | - | Present when the scroll arrow is animating in. |
| data-ending-style | - | Present when the scroll arrow is animating out. |
### ScrollDownArrow
An element that scrolls the select popup down when hovered. Does not render when using touch input.
Renders a `<div>` element.
**ScrollDownArrow Props:**
| Prop | Type | Default | Description |
| :---------- | :------------------------------------------------------------------------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Select.ScrollDownArrow.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Select.ScrollDownArrow.State) => CSSProperties \| undefined)` | - | - |
| keepMounted | `boolean` | `false` | Whether to keep the HTML element in the DOM while the select popup is not scrollable. |
| render | `ReactElement \| ((props: HTMLProps, state: Select.ScrollDownArrow.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**ScrollDownArrow Data Attributes:**
| Attribute | Type | Description |
| :------------------ | :----------------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| data-direction | `'down'` | Indicates the direction of the scroll arrow. |
| data-side | `'none' \| 'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the popup is positioned relative to the trigger. |
| data-visible | - | Present when the scroll arrow is visible. |
| data-starting-style | - | Present when the scroll arrow is animating in. |
| data-ending-style | - | Present when the scroll arrow is animating out. |
### Separator
A separator element accessible to screen readers.
Renders a `<div>` element.
**Separator Props:**
| Prop | Type | Default | Description |
| :---------- | :----------------------------------------------------------------------------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| orientation | `Orientation` | `'horizontal'` | The orientation of the separator. |
| className | `string \| ((state: Separator.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Separator.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Separator.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
references/separator.md
---
title: Separator
subtitle: A separator element accessible to screen readers.
description: A high-quality, unstyled React separator component that is accessible to screen readers.
---
# Separator
A high-quality, unstyled React separator component that is accessible to screen readers.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import { Separator } from '@base-ui/react/separator';
export default function ExampleSeparator() {
return (
<div className="flex gap-4 text-nowrap">
<a
href="#"
className="text-sm text-gray-900 decoration-gray-400 decoration-1 underline-offset-2 outline-none hover:underline focus-visible:rounded-sm focus-visible:no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-800"
>
Home
</a>
<a
href="#"
className="text-sm text-gray-900 decoration-gray-400 decoration-1 underline-offset-2 outline-none hover:underline focus-visible:rounded-sm focus-visible:no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-800"
>
Pricing
</a>
<a
href="#"
className="text-sm text-gray-900 decoration-gray-400 decoration-1 underline-offset-2 outline-none hover:underline focus-visible:rounded-sm focus-visible:no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-800"
>
Blog
</a>
<a
href="#"
className="text-sm text-gray-900 decoration-gray-400 decoration-1 underline-offset-2 outline-none hover:underline focus-visible:rounded-sm focus-visible:no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-800"
>
Support
</a>
<Separator orientation="vertical" className="w-px bg-gray-300" />
<a
href="#"
className="text-sm text-gray-900 decoration-gray-400 decoration-1 underline-offset-2 outline-none hover:underline focus-visible:rounded-sm focus-visible:no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-800"
>
Log in
</a>
<a
href="#"
className="text-sm text-gray-900 decoration-gray-400 decoration-1 underline-offset-2 outline-none hover:underline focus-visible:rounded-sm focus-visible:no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-800"
>
Sign up
</a>
</div>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Container {
display: flex;
gap: 1rem;
text-wrap: nowrap;
}
.Separator {
width: 1px;
background-color: var(--color-gray-300);
}
.Link {
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--color-gray-900);
text-decoration-color: var(--color-gray-400);
text-decoration-thickness: 1px;
text-decoration-line: none;
text-underline-offset: 2px;
@media (hover: hover) {
&:hover {
text-decoration-line: underline;
}
}
&:focus-visible {
border-radius: 0.125rem;
outline: 2px solid var(--color-blue);
text-decoration-line: none;
}
}
```
```tsx
/* index.tsx */
import { Separator } from '@base-ui/react/separator';
import styles from './index.module.css';
export default function ExampleSeparator() {
return (
<div className={styles.Container}>
<a href="#" className={styles.Link}>
Home
</a>
<a href="#" className={styles.Link}>
Pricing
</a>
<a href="#" className={styles.Link}>
Blog
</a>
<a href="#" className={styles.Link}>
Support
</a>
<Separator orientation="vertical" className={styles.Separator} />
<a href="#" className={styles.Link}>
Log in
</a>
<a href="#" className={styles.Link}>
Sign up
</a>
</div>
);
}
```
## Anatomy
Import the component and use it as a single part:
```jsx title="Anatomy"
import { Separator } from '@base-ui/react/separator';
<Separator />;
```
## API reference
A separator element accessible to screen readers.
Renders a `<div>` element.
**Separator Props:**
| Prop | Type | Default | Description |
| :---------- | :----------------------------------------------------------------------------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| orientation | `Orientation` | `'horizontal'` | The orientation of the separator. |
| className | `string \| ((state: Separator.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Separator.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Separator.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
references/slider.md
---
title: Slider
subtitle: An easily stylable range input.
description: A high-quality, unstyled React slider component that works like a range input and is easy to style.
---
# Slider
A high-quality, unstyled React slider component that works like a range input and is easy to style.
## v1.3.0 notes
- `Slider` adds a `Label` part, which makes slider labeling easier without relying only on `aria-label` on each thumb.
- For multi-thumb sliders, still verify that each thumb has a meaningful accessible name when the shared label alone is not enough.
## v1.4.0 notes
- Keyboard floating-point rounding was fixed, which matters for precise step-based sliders.
- Extra TalkBack focus targets were removed, so Android accessibility behavior should be retested before shipping custom ARIA workarounds.
- Prehydration client-only scripts are skipped more safely, reducing hydration noise in SSR setups.
## v1.9.0 notes
- Label focus works in Shadow DOM.
- Drags end on `touchcancel` and `pointercancel`, touch track taps fire `onValueCommitted`, and single-element array values are preserved on pointer interaction.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import { Slider } from "@base-ui/react/slider";
export default function ExampleSlider() {
return (
<Slider.Root defaultValue={25}>
<Slider.Control className="flex w-56 touch-none items-center py-3 select-none">
<Slider.Track className="h-1 w-full rounded bg-gray-200 shadow-[inset_0_0_0_1px] shadow-gray-200 select-none">
<Slider.Indicator className="rounded bg-gray-700 select-none" />
<Slider.Thumb aria-label="Volume" className="size-4 rounded-full bg-white outline outline-1 outline-gray-300 select-none has-[:focus-visible]:outline has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-blue-800" />
</Slider.Track>
</Slider.Control>
</Slider.Root>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Control {
box-sizing: border-box;
display: flex;
align-items: center;
width: 14rem;
padding-block: 0.75rem;
touch-action: none;
user-select: none;
}
.Track {
width: 100%;
height: 0.25rem;
background-color: var(--color-gray-200);
box-shadow: inset 0 0 0 1px var(--color-gray-200);
border-radius: 0.25rem;
user-select: none;
}
.Indicator {
border-radius: 0.25rem;
background-color: var(--color-gray-700);
user-select: none;
}
.Thumb {
width: 1rem;
height: 1rem;
border-radius: 100%;
background-color: white;
outline: 1px solid var(--color-gray-300);
user-select: none;
&:has(:focus-visible) {
outline: 2px solid var(--color-blue);
}
}
```
```tsx
/* index.tsx */
import { Slider } from "@base-ui/react/slider";
import styles from "./index.module.css";
export default function ExampleSlider() {
return (
<Slider.Root defaultValue={25}>
<Slider.Control className={styles.Control}>
<Slider.Track className={styles.Track}>
<Slider.Indicator className={styles.Indicator} />
<Slider.Thumb aria-label="Volume" className={styles.Thumb} />
</Slider.Track>
</Slider.Control>
</Slider.Root>
);
}
```
## Usage guidelines
- **Form controls must have an accessible name**: See [Labeling a slider](/react/components/slider.md) and the [forms guide](/react/handbook/forms.md).
## Anatomy
Import the component and assemble its parts:
```jsx title="Anatomy"
import { Slider } from "@base-ui/react/slider";
<Slider.Root>
<Slider.Value />
<Slider.Control>
<Slider.Track>
<Slider.Indicator />
<Slider.Thumb />
</Slider.Track>
</Slider.Control>
</Slider.Root>;
```
## Examples
### Range slider
To create a range slider:
1. Pass an array of values and place a `<Slider.Thumb>` for each value in the array
2. Additionally for server-side rendering, specify a numeric `index` for each thumb that corresponds to the index of its value in the value array
Thumbs can be configured to behave differently when they collide during pointer interactions using the `thumbCollisionBehavior` prop on `<Slider.Root>`.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import { Slider } from "@base-ui/react/slider";
export default function RangeSlider() {
return (
<Slider.Root defaultValue={[25, 45]}>
<Slider.Control className="flex w-56 touch-none items-center py-3 select-none">
<Slider.Track className="h-1 w-full rounded bg-gray-200 shadow-[inset_0_0_0_1px] shadow-gray-200 select-none">
<Slider.Indicator className="rounded bg-gray-700 select-none" />
<Slider.Thumb index={0} className="size-4 rounded-full bg-white outline outline-1 outline-gray-300 select-none has-[:focus-visible]:outline has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-blue-800" />
<Slider.Thumb index={1} className="size-4 rounded-full bg-white outline outline-1 outline-gray-300 select-none has-[:focus-visible]:outline has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-blue-800" />
</Slider.Track>
</Slider.Control>
</Slider.Root>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Control {
box-sizing: border-box;
display: flex;
align-items: center;
width: 14rem;
padding-block: 0.75rem;
touch-action: none;
user-select: none;
}
.Track {
width: 100%;
height: 0.25rem;
background-color: var(--color-gray-200);
box-shadow: inset 0 0 0 1px var(--color-gray-200);
border-radius: 0.25rem;
user-select: none;
}
.Indicator {
border-radius: 0.25rem;
background-color: var(--color-gray-700);
user-select: none;
}
.Thumb {
width: 1rem;
height: 1rem;
border-radius: 100%;
background-color: white;
outline: 1px solid var(--color-gray-300);
user-select: none;
&:has(:focus-visible) {
outline: 2px solid var(--color-blue);
}
}
```
```tsx
/* index.tsx */
import { Slider } from "@base-ui/react/slider";
import styles from "./index.module.css";
export default function RangeSlider() {
return (
<Slider.Root defaultValue={[25, 45]}>
<Slider.Control className={styles.Control}>
<Slider.Track className={styles.Track}>
<Slider.Indicator className={styles.Indicator} />
<Slider.Thumb index={0} className={styles.Thumb} />
<Slider.Thumb index={1} className={styles.Thumb} />
</Slider.Track>
</Slider.Control>
</Slider.Root>
);
}
```
### Thumb alignment
Set `thumbAlignment="edge"` to inset the thumb such that its edge aligns with the edge of the control when the value is at `min` or `max`, without overflowing the control like the default `"center"` alignment.
A client-only alternative `thumbAlignment="edge-client-only"` can be used to reduce bundle size but only renders after React hydration.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import { Slider } from "@base-ui/react/slider";
export default function EdgeAlignedThumb() {
return (
<Slider.Root thumbAlignment="edge" defaultValue={25}>
<Slider.Control className="flex w-56 touch-none items-center py-3 select-none">
<Slider.Track className="h-1 w-full rounded bg-gray-200 shadow-[inset_0_0_0_1px] shadow-gray-200 select-none">
<Slider.Indicator className="rounded bg-gray-700 select-none" />
<Slider.Thumb className="size-4 rounded-full bg-white outline outline-1 outline-gray-300 select-none has-[:focus-visible]:outline has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-blue-800" />
</Slider.Track>
</Slider.Control>
</Slider.Root>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Control {
box-sizing: border-box;
display: flex;
align-items: center;
width: 14rem;
padding-block: 0.75rem;
touch-action: none;
user-select: none;
}
.Track {
width: 100%;
height: 0.25rem;
background-color: var(--color-gray-200);
box-shadow: inset 0 0 0 1px var(--color-gray-200);
border-radius: 0.25rem;
user-select: none;
}
.Indicator {
border-radius: 0.25rem;
background-color: var(--color-gray-700);
user-select: none;
}
.Thumb {
width: 1rem;
height: 1rem;
border-radius: 100%;
background-color: white;
outline: 1px solid var(--color-gray-300);
user-select: none;
&:has(:focus-visible) {
outline: 2px solid var(--color-blue);
}
}
```
```tsx
/* index.tsx */
import { Slider } from "@base-ui/react/slider";
import styles from "./index.module.css";
export default function EdgeAlignedThumb() {
return (
<Slider.Root thumbAlignment="edge" defaultValue={25}>
<Slider.Control className={styles.Control}>
<Slider.Track className={styles.Track}>
<Slider.Indicator className={styles.Indicator} />
<Slider.Thumb className={styles.Thumb} />
</Slider.Track>
</Slider.Control>
</Slider.Root>
);
}
```
### Labeling a slider
A single-thumb slider without a visible label (such as a volume control) can be labeled using `aria-label` on `<Slider.Thumb>`:
```tsx title="Slider with invisible label" {5}
<Slider.Root>
<Slider.Control>
<Slider.Track>
<Slider.Indicator />
<Slider.Thumb aria-label="Volume" />
</Slider.Track>
</Slider.Control>
</Slider.Root>
```
A visible label can be created using `aria-labelledby` on `<Slider.Root>` that references the `id` of a sibling element, such as a `<div>`:
```tsx title="Slider with visible label" {1,2}
<div id="volume-label">Volume</div>
<Slider.Root aria-labelledby="volume-label">
<Slider.Control>
<Slider.Track>
<Slider.Indicator />
<Slider.Thumb />
</Slider.Track>
</Slider.Control>
</Slider.Root>
```
For a multi-thumb range slider with a visible label, add `aria-label` on each `<Slider.Thumb>` to distinguish them:
```tsx title="Labeling multi-thumb range sliders" {6-7}
<div id="price-label">Price range</div>
<Slider.Root defaultValue={[25, 75]} aria-labelledby="price-label">
<Slider.Control>
<Slider.Track>
<Slider.Indicator />
<Slider.Thumb index={0} aria-label="Minimum price" />
<Slider.Thumb index={1} aria-label="Maximum price" />
</Slider.Track>
</Slider.Control>
</Slider.Root>
```
The [Field](/react/components/field.md) and [Fieldset](/react/components/fieldset.md) components can also be used to provide accessible labels for sliders, eliminating the need to track `id` or `aria-labelledby` associations manually. Field comes with a UX improvement to prevent double clicks from selecting the label text.
For a single-thumb slider, use [Field](/react/components/field.md) to provide a visible label:
```tsx title="Using Field to label a slider" {2}
<Field.Root>
<Field.Label>Volume</Field.Label>
<Slider.Root>
<Slider.Control>
<Slider.Track>
<Slider.Indicator />
<Slider.Thumb />
</Slider.Track>
</Slider.Control>
</Slider.Root>
</Field.Root>
```
For a multi-thumb range slider, use [Fieldset](/react/components/fieldset.md) by replacing the `<Fieldset.Root>` element with `<Slider.Root>` via the `render` prop. `<Fieldset.Legend>` provides the visible label for the group:
```tsx title="Using Fieldset to label a multi-thumb slider" {2,3,7-8}
<Field.Root>
<Fieldset.Root render={<Slider.Root />}>
<Fieldset.Legend>Price range</Fieldset.Legend>
<Slider.Control>
<Slider.Track>
<Slider.Indicator />
<Slider.Thumb index={0} aria-label="Minimum price" />
<Slider.Thumb index={1} aria-label="Maximum price" />
</Slider.Track>
</Slider.Control>
</Fieldset.Root>
</Field.Root>
```
### Form integration
To use a slider in a form, pass the slider's `name` to `<Field.Root>`:
```tsx title="Using Slider in a form" {2}
<Form>
<Field.Root name="volume">
<Field.Label>Volume</Field.Label>
<Slider.Root>
<Slider.Control>
<Slider.Track>
<Slider.Indicator />
<Slider.Thumb />
</Slider.Track>
</Slider.Control>
</Slider.Root>
</Field.Root>
</Form>
```
## API reference
### Root
Groups all parts of the slider.
Renders a `<div>` element.
**Root Props:**
| Prop | Type | Default | Description |
| :------------ | :------------------------------------------------------------------------------------ | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | `string` | - | Identifies the field when a form is submitted. |
| defaultValue | `number \| number[]` | - | The uncontrolled value of the slider when it’s initially rendered.To render a controlled slider, use the `value` prop instead. |
| value | `number \| number[]` | - | The value of the slider.
For ranged sliders, provide an array with two values. |
| onValueChange | `((value: number \| number[], eventDetails: Slider.Root.ChangeEventDetails) => void)` | - | Callback function that is fired when the slider's value changed.
You can pull out the new value by accessing `event.target.value` (any).The `eventDetails.reason` indicates what triggered the change:\* `'input-change'` when the hidden range input emits a change event (for example, via form integration) |
- `'track-press'` when the control track is pressed
- `'drag'` while dragging a thumb
- `'keyboard'` for keyboard input
- `'none'` when the change is triggered without a specific interaction |
| onValueCommitted | `((value: number \| number[], eventDetails: Slider.Root.CommitEventDetails) => void)` | - | Callback function that is fired when the `pointerup` is triggered.
**Warning**: This is a generic event not a change event.The `eventDetails.reason` indicates what triggered the commit:\* `'drag'` while dragging a thumb
- `'track-press'` when the control track is pressed
- `'keyboard'` for keyboard input
- `'input-change'` when the hidden range input emits a change event (for example, via form integration)
- `'none'` when the commit occurs without a specific interaction |
| locale | `Intl.LocalesArgument` | - | The locale used by `Intl.NumberFormat` when formatting the value.
Defaults to the user's runtime locale. |
| thumbAlignment | `'center' \| 'edge' \| 'edge-client-only'` | `'center'` | How the thumb(s) are aligned relative to `Slider.Control` when the value is at `min` or `max`:\* `center`: The center of the thumb is aligned with the control edge
- `edge`: The thumb is inset within the control such that its edge is aligned with the control edge
- `edge-client-only`: Same as `edge` but renders after React hydration on the client, reducing bundle size in return |
| thumbCollisionBehavior | `'none' \| 'push' \| 'swap'` | `'push'` | Controls how thumbs behave when they collide during pointer interactions.\* `'push'` (default): Thumbs push each other without restoring their previous positions when dragged back.
- `'swap'`: Thumbs swap places when dragged past each other.
- `'none'`: Thumbs cannot move past each other; excess movement is ignored. |
| step | `number` | `1` | The granularity with which the slider can step through values. (A "discrete" slider.)
The `min` prop serves as the origin for the valid values.
We recommend (max - min) to be evenly divisible by the step. |
| largeStep | `number` | `10` | The granularity with which the slider can step through values when using Page Up/Page Down or Shift + Arrow Up/Arrow Down. |
| minStepsBetweenValues | `number` | `0` | The minimum steps between values in a range slider. |
| min | `number` | `0` | The minimum allowed value of the slider.
Should not be equal to max. |
| max | `number` | `100` | The maximum allowed value of the slider.
Should not be equal to min. |
| format | `Intl.NumberFormatOptions` | - | Options to format the input value. |
| disabled | `boolean` | `false` | Whether the slider should ignore user interaction. |
| orientation | `Orientation` | `'horizontal'` | The component orientation. |
| className | `string \| ((state: Slider.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Slider.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Slider.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Root Data Attributes:**
| Attribute | Type | Description |
| :--------------- | :--------------------------- | :------------------------------------------------------------------------ |
| data-dragging | - | Present while the user is dragging. |
| data-orientation | `'horizontal' \| 'vertical'` | Indicates the orientation of the slider. |
| data-disabled | - | Present when the slider is disabled. |
| data-valid | - | Present when the slider is in valid state (when wrapped in Field.Root). |
| data-invalid | - | Present when the slider is in invalid state (when wrapped in Field.Root). |
| data-dirty | - | Present when the slider's value has changed (when wrapped in Field.Root). |
| data-touched | - | Present when the slider has been touched (when wrapped in Field.Root). |
| data-focused | - | Present when the slider is focused (when wrapped in Field.Root). |
### Value
Displays the current value of the slider as text.
Renders an `<output>` element.
**Value Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| children | `((formattedValues: string[], values: number[]) => ReactNode) \| null` | - | - |
| className | `string \| ((state: Slider.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Slider.Root.State) => CSSProperties \| undefined)` | - | \* |
| render | `ReactElement \| ((props: HTMLProps, state: Slider.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Value Data Attributes:**
| Attribute | Type | Description |
| :--------------- | :--------------------------- | :------------------------------------------------------------------------ |
| data-dragging | - | Present while the user is dragging. |
| data-orientation | `'horizontal' \| 'vertical'` | Indicates the orientation of the slider. |
| data-disabled | - | Present when the slider is disabled. |
| data-valid | - | Present when the slider is in valid state (when wrapped in Field.Root). |
| data-invalid | - | Present when the slider is in invalid state (when wrapped in Field.Root). |
| data-dirty | - | Present when the slider's value has changed (when wrapped in Field.Root). |
| data-touched | - | Present when the slider has been touched (when wrapped in Field.Root). |
| data-focused | - | Present when the slider is focused (when wrapped in Field.Root). |
### Control
The clickable, interactive part of the slider.
Renders a `<div>` element.
**Control Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Slider.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Slider.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Slider.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Control Data Attributes:**
| Attribute | Type | Description |
| :--------------- | :--------------------------- | :------------------------------------------------------------------------ |
| data-dragging | - | Present while the user is dragging. |
| data-orientation | `'horizontal' \| 'vertical'` | Indicates the orientation of the slider. |
| data-disabled | - | Present when the slider is disabled. |
| data-valid | - | Present when the slider is in valid state (when wrapped in Field.Root). |
| data-invalid | - | Present when the slider is in invalid state (when wrapped in Field.Root). |
| data-dirty | - | Present when the slider's value has changed (when wrapped in Field.Root). |
| data-touched | - | Present when the slider has been touched (when wrapped in Field.Root). |
| data-focused | - | Present when the slider is focused (when wrapped in Field.Root). |
### Track
Contains the slider indicator and represents the entire range of the slider.
Renders a `<div>` element.
**Track Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Slider.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Slider.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Slider.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Track Data Attributes:**
| Attribute | Type | Description |
| :--------------- | :--------------------------- | :------------------------------------------------------------------------ |
| data-dragging | - | Present while the user is dragging. |
| data-orientation | `'horizontal' \| 'vertical'` | Indicates the orientation of the slider. |
| data-disabled | - | Present when the slider is disabled. |
| data-valid | - | Present when the slider is in valid state (when wrapped in Field.Root). |
| data-invalid | - | Present when the slider is in invalid state (when wrapped in Field.Root). |
| data-dirty | - | Present when the slider's value has changed (when wrapped in Field.Root). |
| data-touched | - | Present when the slider has been touched (when wrapped in Field.Root). |
| data-focused | - | Present when the slider is focused (when wrapped in Field.Root). |
### Indicator
Visualizes the current value of the slider.
Renders a `<div>` element.
**Indicator Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Slider.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Slider.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Slider.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Indicator Data Attributes:**
| Attribute | Type | Description |
| :--------------- | :--------------------------- | :------------------------------------------------------------------------ |
| data-dragging | - | Present while the user is dragging. |
| data-orientation | `'horizontal' \| 'vertical'` | Indicates the orientation of the slider. |
| data-disabled | - | Present when the slider is disabled. |
| data-valid | - | Present when the slider is in valid state (when wrapped in Field.Root). |
| data-invalid | - | Present when the slider is in invalid state (when wrapped in Field.Root). |
| data-dirty | - | Present when the slider's value has changed (when wrapped in Field.Root). |
| data-touched | - | Present when the slider has been touched (when wrapped in Field.Root). |
| data-focused | - | Present when the slider is focused (when wrapped in Field.Root). |
### Thumb
The draggable part of the slider at the tip of the indicator.
Renders a `<div>` element and a nested `<input type="range">`.
**Thumb Props:**
| Prop | Type | Default | Description |
| :--------------- | :-------------------------------------------------------------------------------- | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| getAriaLabel | `((index: number) => string) \| null` | - | A function which returns a string value for the [`aria-label`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label) attribute of the `input`. |
| getAriaValueText | `((formattedValue: string, value: number, index: number) => string) \| null` | - | A function which returns a string value for the [`aria-valuetext`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-valuetext) attribute of the `input`.
This is important for screen reader users. |
| index | `number` | - | The index of the thumb which corresponds to the index of its value in the
`value` or `defaultValue` array.
This prop is required to support server-side rendering for range sliders
with multiple thumbs. |
| onBlur | `FocusEventHandler<HTMLInputElement>` | - | A blur handler forwarded to the `input`. |
| onFocus | `FocusEventHandler<HTMLInputElement>` | - | A focus handler forwarded to the `input`. |
| tabIndex | `number` | - | Optional tab index attribute forwarded to the `input`. |
| disabled | `boolean` | `false` | Whether the thumb should ignore user interaction. |
| inputRef | `Ref<HTMLInputElement>` | - | A ref to access the nested input element. |
| className | `string \| ((state: Slider.Thumb.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Slider.Thumb.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Slider.Thumb.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Thumb Data Attributes:**
| Attribute | Type | Description |
| :--------------- | :--------------------------- | :------------------------------------------------------------------------ |
| data-dragging | - | Present while the user is dragging. |
| data-orientation | `'horizontal' \| 'vertical'` | Indicates the orientation of the slider. |
| data-disabled | - | Present when the slider is disabled. |
| data-valid | - | Present when the slider is in valid state (when wrapped in Field.Root). |
| data-invalid | - | Present when the slider is in invalid state (when wrapped in Field.Root). |
| data-dirty | - | Present when the slider's value has changed (when wrapped in Field.Root). |
| data-touched | - | Present when the slider has been touched (when wrapped in Field.Root). |
| data-focused | - | Present when the slider is focused (when wrapped in Field.Root). |
| data-index | - | Indicates the index of the thumb in range sliders. |
references/switch.md
---
title: Switch
subtitle: A control that indicates whether a setting is on or off.
description: A high-quality, unstyled React switch component that indicates whether a setting is on or off.
---
# Switch
A high-quality, unstyled React switch component that indicates whether a setting is on or off.
## v1.4.0 notes
- `readOnly` mode now blocks input state changes instead of allowing accidental toggles through the hidden input path.
## v1.9.0 notes
- `aria-label` is preferred over an associated label when both are present, so an explicit `aria-label` on the switch wins.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import { Switch } from "@base-ui/react/switch";
export default function ExampleSwitch() {
return (
<label className="flex items-center gap-2 text-base text-gray-900">
<Switch.Root defaultChecked className="relative flex h-6 w-10 rounded-full bg-gradient-to-r from-gray-700 from-35% to-gray-200 to-65% bg-[length:6.5rem_100%] bg-[100%_0%] bg-no-repeat p-px shadow-[inset_0_1.5px_2px] shadow-gray-200 outline outline-1 -outline-offset-1 outline-gray-200 transition-[background-position,box-shadow] duration-[125ms] ease-[cubic-bezier(0.26,0.75,0.38,0.45)] before:absolute before:rounded-full before:outline-offset-2 before:outline-blue-800 focus-visible:before:inset-0 focus-visible:before:outline focus-visible:before:outline-2 active:bg-gray-100 data-[checked]:bg-[0%_0%] data-[checked]:active:bg-gray-500 dark:from-gray-500 dark:shadow-black/75 dark:outline-white/15 dark:data-[checked]:shadow-none">
<Switch.Thumb className="aspect-square h-full rounded-full bg-white shadow-[0_0_1px_1px,0_1px_1px,1px_2px_4px_-1px] shadow-gray-100 transition-transform duration-150 data-[checked]:translate-x-4 dark:shadow-black/25" />
</Switch.Root>
Notifications
</label>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Label {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1rem;
line-height: 1.5rem;
color: var(--color-gray-900);
}
.Switch {
position: relative;
display: flex;
appearance: none;
border: 0;
margin: 0;
padding: 1px;
width: 2.5rem;
height: 1.5rem;
border-radius: 1.5rem;
outline: 1px solid;
outline-offset: -1px;
background-color: transparent;
background-image: linear-gradient(to right, var(--color-gray-700) 35%, var(--color-gray-200) 65%);
background-size: 6.5rem 100%;
background-position-x: 100%;
background-repeat: no-repeat;
transition-property: background-position, box-shadow;
transition-timing-function: cubic-bezier(0.26, 0.75, 0.38, 0.45);
transition-duration: 125ms;
&:active {
background-color: var(--color-gray-100);
}
&[data-checked] {
background-position-x: 0%;
}
&[data-checked]:active {
background-color: var(--color-gray-500);
}
@media (prefers-color-scheme: light) {
box-shadow: var(--color-gray-200) 0 1.5px 2px inset;
outline-color: var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
box-shadow: rgb(0 0 0 / 75%) 0 1.5px 2px inset;
outline-color: rgb(255 255 255 / 15%);
background-image: linear-gradient(to right, var(--color-gray-500) 35%, var(--color-gray-200) 65%);
&[data-checked] {
box-shadow: none;
}
}
&:focus-visible {
&::before {
content: "";
inset: 0;
position: absolute;
border-radius: inherit;
outline: 2px solid var(--color-blue);
outline-offset: 2px;
}
}
}
.Thumb {
aspect-ratio: 1 / 1;
height: 100%;
border-radius: 100%;
background-color: white;
transition: translate 150ms ease;
&[data-checked] {
translate: 1rem 0;
}
@media (prefers-color-scheme: light) {
box-shadow:
0 0 1px 1px var(--color-gray-100),
0 1px 1px var(--color-gray-100),
1px 2px 4px -1px var(--color-gray-100);
}
@media (prefers-color-scheme: dark) {
box-shadow:
0 0 1px 1px rgb(0 0 0 / 25%),
0 1px 1px rgb(0 0 0 / 25%),
1px 2px 4px -1px rgb(0 0 0 / 25%);
}
}
```
```tsx
/* index.tsx */
import { Switch } from "@base-ui/react/switch";
import styles from "./index.module.css";
export default function ExampleSwitch() {
return (
<label className={styles.Label}>
<Switch.Root defaultChecked className={styles.Switch}>
<Switch.Thumb className={styles.Thumb} />
</Switch.Root>
Notifications
</label>
);
}
```
## Usage guidelines
- **Form controls must have an accessible name**: It can be created using a `<label>` element or the `Field` component. See [Labeling a switch](/react/components/switch.md) and the [forms guide](/react/handbook/forms.md).
## Anatomy
Import the component and assemble its parts:
```jsx title="Anatomy"
import { Switch } from "@base-ui/react/switch";
<Switch.Root>
<Switch.Thumb />
</Switch.Root>;
```
## Examples
### Labeling a switch
Label a switch using an enclosing `<label>` element that wraps `<Switch.Root>`. An enclosing label is announced to screen readers when focus is on the control, and ensures that clicking on any gaps between the label and switch still toggles the control.
```tsx title="Using an enclosing label to label a switch" {1,4}
<label>
<Switch.Root />
Notifications
</label>
```
The [Field](/react/components/field.md) component eliminates the need to track `id` or `aria-label` associations manually. It supports both enclosing and sibling labeling patterns, along with a UX improvement to prevent double clicks from selecting the label text.
```tsx title="Using the Field component to label a switch" {2,5}
<Field.Root>
<Field.Label>
<Switch.Root />
Notifications
</Field.Label>
</Field.Root>
```
### Form integration
To use a switch in a form, pass the switch's `name` to `<Field.Root>`:
```tsx title="Using Switch in a form" {2}
<Form>
<Field.Root name="notifications">
<Field.Label>
<Switch.Root />
Notifications
</Field.Label>
</Field.Root>
</Form>
```
## API reference
### Root
Represents the switch itself.
Renders a `<span>` element and a hidden `<input>` beside.
**Root Props:**
| Prop | Type | Default | Description |
| :-------------- | :------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | `string` | - | Identifies the field when a form is submitted. |
| defaultChecked | `boolean` | `false` | Whether the switch is initially active. To render a controlled switch, use the `checked` prop instead. |
| checked | `boolean` | - | Whether the switch is currently active. To render an uncontrolled switch, use the `defaultChecked` prop instead. |
| onCheckedChange | `((checked: boolean, eventDetails: Switch.Root.ChangeEventDetails) => void)` | - | Event handler called when the switch is activated or deactivated. |
| value | `string` | - | The value submitted with the form when the switch is on.
By default, switch submits the "on" value, matching native checkbox behavior. |
| nativeButton | `boolean` | `false` | Whether the component renders a native `<button>` element when replacing it
via the `render` prop.
Set to `true` if the rendered element is a native button. |
| uncheckedValue | `string` | - | The value submitted with the form when the switch is off.
By default, unchecked switches do not submit any value, matching native checkbox behavior. |
| disabled | `boolean` | `false` | Whether the component should ignore user interaction. |
| readOnly | `boolean` | `false` | Whether the user should be unable to activate or deactivate the switch. |
| required | `boolean` | `false` | Whether the user must activate the switch before submitting a form. |
| inputRef | `Ref<HTMLInputElement>` | - | A ref to access the hidden `<input>` element. |
| id | `string` | - | The id of the switch element. |
| className | `string \| ((state: Switch.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Switch.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Switch.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Root Data Attributes:**
| Attribute | Type | Description |
| :------------- | :--- | :------------------------------------------------------------------------ |
| data-checked | - | Present when the switch is checked. |
| data-unchecked | - | Present when the switch is not checked. |
| data-disabled | - | Present when the switch is disabled. |
| data-readonly | - | Present when the switch is readonly. |
| data-required | - | Present when the switch is required. |
| data-valid | - | Present when the switch is in valid state (when wrapped in Field.Root). |
| data-invalid | - | Present when the switch is in invalid state (when wrapped in Field.Root). |
| data-dirty | - | Present when the switch's value has changed (when wrapped in Field.Root). |
| data-touched | - | Present when the switch has been touched (when wrapped in Field.Root). |
| data-filled | - | Present when the switch is active (when wrapped in Field.Root). |
| data-focused | - | Present when the switch is focused (when wrapped in Field.Root). |
### Thumb
The movable part of the switch that indicates whether the switch is on or off.
Renders a `<span>`.
**Thumb Props:**
| Prop | Type | Default | Description |
| :-------- | :-------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Switch.Thumb.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Switch.Thumb.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Switch.Thumb.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Thumb Data Attributes:**
| Attribute | Type | Description |
| :------------- | :--- | :------------------------------------------------------------------------ |
| data-checked | - | Present when the switch is checked. |
| data-unchecked | - | Present when the switch is not checked. |
| data-disabled | - | Present when the switch is disabled. |
| data-readonly | - | Present when the switch is readonly. |
| data-required | - | Present when the switch is required. |
| data-valid | - | Present when the switch is in valid state (when wrapped in Field.Root). |
| data-invalid | - | Present when the switch is in invalid state (when wrapped in Field.Root). |
| data-dirty | - | Present when the switch's value has changed (when wrapped in Field.Root). |
| data-touched | - | Present when the switch has been touched (when wrapped in Field.Root). |
| data-filled | - | Present when the switch is active (when wrapped in Field.Root). |
| data-focused | - | Present when the switch is focused (when wrapped in Field.Root). |
references/tabs.md
---
title: Tabs
subtitle: A component for toggling between related panels on the same page.
description: A high-quality, unstyled React tabs component for toggling between related panels on the same page.
---
# Tabs
A high-quality, unstyled React tabs component for toggling between related panels on the same page.
## v1.9.0 notes
- Tabs with function values can be selected correctly, so a `value` that is a function is no longer compared by reference alone.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import * as React from 'react';
import { Tabs } from '@base-ui/react/tabs';
export default function ExampleTabs() {
return (
<Tabs.Root className="rounded-md border border-gray-200" defaultValue="overview">
<Tabs.List className="relative z-0 flex gap-1 px-1 shadow-[inset_0_-1px] shadow-gray-200">
<Tabs.Tab
className="flex h-8 items-center justify-center border-0 px-2 text-sm font-medium break-keep whitespace-nowrap text-gray-600 outline-none select-none before:inset-x-0 before:inset-y-1 before:rounded-sm before:-outline-offset-1 before:outline-blue-800 hover:text-gray-900 focus-visible:relative focus-visible:before:absolute focus-visible:before:outline focus-visible:before:outline-2 data-[active]:text-gray-900"
value="overview"
>
Overview
</Tabs.Tab>
<Tabs.Tab
className="flex h-8 items-center justify-center border-0 px-2 text-sm font-medium break-keep whitespace-nowrap text-gray-600 outline-none select-none before:inset-x-0 before:inset-y-1 before:rounded-sm before:-outline-offset-1 before:outline-blue-800 hover:text-gray-900 focus-visible:relative focus-visible:before:absolute focus-visible:before:outline focus-visible:before:outline-2 data-[active]:text-gray-900"
value="projects"
>
Projects
</Tabs.Tab>
<Tabs.Tab
className="flex h-8 items-center justify-center border-0 px-2 text-sm font-medium break-keep whitespace-nowrap text-gray-600 outline-none select-none before:inset-x-0 before:inset-y-1 before:rounded-sm before:-outline-offset-1 before:outline-blue-800 hover:text-gray-900 focus-visible:relative focus-visible:before:absolute focus-visible:before:outline focus-visible:before:outline-2 data-[active]:text-gray-900"
value="account"
>
Account
</Tabs.Tab>
<Tabs.Indicator className="absolute top-1/2 left-0 z-[-1] h-6 w-[var(--active-tab-width)] translate-x-[var(--active-tab-left)] -translate-y-1/2 rounded-sm bg-gray-100 transition-all duration-200 ease-in-out" />
</Tabs.List>
<Tabs.Panel
className="relative flex h-32 items-center justify-center -outline-offset-1 outline-blue-800 focus-visible:rounded-md focus-visible:outline focus-visible:outline-2"
value="overview"
>
<OverviewIcon className="size-10 text-gray-300" />
</Tabs.Panel>
<Tabs.Panel
className="relative flex h-32 items-center justify-center -outline-offset-1 outline-blue-800 focus-visible:rounded-md focus-visible:outline focus-visible:outline-2"
value="projects"
>
<ProjectIcon className="size-10 text-gray-300" />
</Tabs.Panel>
<Tabs.Panel
className="relative flex h-32 items-center justify-center -outline-offset-1 outline-blue-800 focus-visible:rounded-md focus-visible:outline focus-visible:outline-2"
value="account"
>
<PersonIcon className="size-10 text-gray-300" />
</Tabs.Panel>
</Tabs.Root>
);
}
function OverviewIcon(props: React.ComponentProps<'svg'>) {
return (
<svg width="40" height="40" viewBox="0 0 30 30" fill="currentcolor" {...props}>
<path d="M 6 4 C 4.895 4 4 4.895 4 6 L 4 12 C 4 13.105 4.895 14 6 14 L 12 14 C 13.105 14 14 13.105 14 12 L 14 6 C 14 4.895 13.105 4 12 4 L 6 4 z M 18 4 C 16.895 4 16 4.895 16 6 L 16 12 C 16 13.105 16.895 14 18 14 L 24 14 C 25.105 14 26 13.105 26 12 L 26 6 C 26 4.895 25.105 4 24 4 L 18 4 z M 9 6 C 10.657 6 12 7.343 12 9 C 12 10.657 10.657 12 9 12 C 7.343 12 6 10.657 6 9 C 6 7.343 7.343 6 9 6 z M 18 6 L 24 6 L 24 12 L 18 12 L 18 6 z M 6 16 C 4.895 16 4 16.895 4 18 L 4 24 C 4 25.105 4.895 26 6 26 L 12 26 C 13.105 26 14 25.105 14 24 L 14 18 C 14 16.895 13.105 16 12 16 L 6 16 z M 18 16 C 16.895 16 16 16.895 16 18 L 16 24 C 16 25.105 16.895 26 18 26 L 24 26 C 25.105 26 26 25.105 26 24 L 26 18 C 26 16.895 25.105 16 24 16 L 18 16 z M 21 17.5 L 24.5 21 L 21 24.5 L 17.5 21 L 21 17.5 z M 9 18 L 11.886719 23 L 6.1132812 23 L 9 18 z" />
</svg>
);
}
function ProjectIcon(props: React.ComponentProps<'svg'>) {
return (
<svg width="40" height="40" viewBox="0 0 30 30" fill="currentcolor" {...props}>
<path d="M 14.984375 1.9863281 A 1.0001 1.0001 0 0 0 14 3 L 14 4 L 5 4 L 4 4 A 1.0001 1.0001 0 1 0 3.9804688 6 C 3.9348612 9.0608831 3.6893807 11.887023 3.1523438 14.142578 C 2.5565033 16.645108 1.6039585 18.395538 0.4453125 19.167969 A 1.0001 1.0001 0 0 0 1 21 L 4 21 C 4 22.105 4.895 23 6 23 L 11.787109 23 L 10.148438 26.042969 A 1.5 1.5 0 0 0 9 27.5 A 1.5 1.5 0 0 0 10.5 29 A 1.5 1.5 0 0 0 12 27.5 A 1.5 1.5 0 0 0 11.910156 26.992188 L 14.060547 23 L 15.939453 23 L 18.089844 26.992188 A 1.5 1.5 0 0 0 18 27.5 A 1.5 1.5 0 0 0 19.5 29 A 1.5 1.5 0 0 0 21 27.5 A 1.5 1.5 0 0 0 19.851562 26.042969 L 18.212891 23 L 24 23 C 25.105 23 26 22.105 26 21 L 26 6 A 1.0001 1.0001 0 1 0 26 4 L 25 4 L 16 4 L 16 3 A 1.0001 1.0001 0 0 0 14.984375 1.9863281 z M 5.9589844 6 L 14.832031 6 A 1.0001 1.0001 0 0 0 15.158203 6 L 23.958984 6 C 23.912194 9.0500505 23.687726 11.893974 23.152344 14.142578 C 22.583328 16.532444 21.674397 18.178754 20.585938 19 L 3.1523438 19 C 3.9976592 17.786874 4.6791735 16.365049 5.0976562 14.607422 C 5.6877248 12.129135 5.9137751 9.1554725 5.9589844 6 z" />
</svg>
);
}
function PersonIcon(props: React.ComponentProps<'svg'>) {
return (
<svg width="40" height="40" viewBox="0 0 30 30" fill="currentcolor" {...props}>
<path d="M15,3C8.373,3,3,8.373,3,15c0,6.627,5.373,12,12,12s12-5.373,12-12C27,8.373,21.627,3,15,3z M8,22.141 c1.167-3.5,4.667-2.134,5.25-4.03v-1.264c-0.262-0.141-1.013-1.109-1.092-1.865c-0.207-0.018-0.531-0.223-0.627-1.034 c-0.051-0.435,0.153-0.68,0.276-0.757c0,0-0.308-0.702-0.308-1.399C11.5,9.72,12.526,8,15,8c1.336,0,1.75,0.947,1.75,0.947 c1.194,0,1.75,1.309,1.75,2.844c0,0.765-0.308,1.399-0.308,1.399c0.124,0.077,0.328,0.322,0.277,0.757 c-0.096,0.811-0.42,1.016-0.627,1.034c-0.079,0.756-0.829,1.724-1.092,1.865v1.264c0.583,1.897,4.083,0.531,5.25,4.031 c0,0-2.618,2.859-7,2.859C10.593,25,8,22.141,8,22.141z" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Tabs {
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
}
.List {
display: flex;
position: relative;
z-index: 0;
padding-inline: 0.25rem;
gap: 0.25rem;
box-shadow: inset 0 -1px var(--color-gray-200);
}
.Tab {
display: flex;
align-items: center;
justify-content: center;
border: 0;
margin: 0;
outline: 0;
background: none;
appearance: none;
color: var(--color-gray-600);
font-family: inherit;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
user-select: none;
white-space: nowrap;
word-break: keep-all;
padding-inline: 0.5rem;
padding-block: 0;
height: 2rem;
&[data-active] {
color: var(--color-gray-900);
}
@media (hover: hover) {
&:hover {
color: var(--color-gray-900);
}
}
&:focus-visible {
position: relative;
&::before {
content: '';
position: absolute;
inset: 0.25rem 0;
border-radius: 0.25rem;
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
}
.Indicator {
position: absolute;
z-index: -1;
left: 0;
top: 50%;
translate: var(--active-tab-left) -50%;
width: var(--active-tab-width);
height: 1.5rem;
border-radius: 0.25rem;
background-color: var(--color-gray-100);
transition-property: translate, width;
transition-duration: 200ms;
transition-timing-function: ease-in-out;
}
.Panel {
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: 8rem;
outline: 0;
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
border-radius: 0.375rem;
}
&[hidden] {
display: none;
}
}
.Icon {
width: 2.5rem;
height: 2.5rem;
color: var(--color-gray-300);
}
```
```tsx
/* index.tsx */
import * as React from 'react';
import { Tabs } from '@base-ui/react/tabs';
import styles from './index.module.css';
export default function ExampleTabs() {
return (
<Tabs.Root className={styles.Tabs} defaultValue="overview">
<Tabs.List className={styles.List}>
<Tabs.Tab className={styles.Tab} value="overview">
Overview
</Tabs.Tab>
<Tabs.Tab className={styles.Tab} value="projects">
Projects
</Tabs.Tab>
<Tabs.Tab className={styles.Tab} value="account">
Account
</Tabs.Tab>
<Tabs.Indicator className={styles.Indicator} />
</Tabs.List>
<Tabs.Panel className={styles.Panel} value="overview">
<OverviewIcon className={styles.Icon} />
</Tabs.Panel>
<Tabs.Panel className={styles.Panel} value="projects">
<ProjectIcon className={styles.Icon} />
</Tabs.Panel>
<Tabs.Panel className={styles.Panel} value="account">
<PersonIcon className={styles.Icon} />
</Tabs.Panel>
</Tabs.Root>
);
}
function OverviewIcon(props: React.ComponentProps<'svg'>) {
return (
<svg width="40" height="40" viewBox="0 0 30 30" fill="currentcolor" {...props}>
<path d="M 6 4 C 4.895 4 4 4.895 4 6 L 4 12 C 4 13.105 4.895 14 6 14 L 12 14 C 13.105 14 14 13.105 14 12 L 14 6 C 14 4.895 13.105 4 12 4 L 6 4 z M 18 4 C 16.895 4 16 4.895 16 6 L 16 12 C 16 13.105 16.895 14 18 14 L 24 14 C 25.105 14 26 13.105 26 12 L 26 6 C 26 4.895 25.105 4 24 4 L 18 4 z M 9 6 C 10.657 6 12 7.343 12 9 C 12 10.657 10.657 12 9 12 C 7.343 12 6 10.657 6 9 C 6 7.343 7.343 6 9 6 z M 18 6 L 24 6 L 24 12 L 18 12 L 18 6 z M 6 16 C 4.895 16 4 16.895 4 18 L 4 24 C 4 25.105 4.895 26 6 26 L 12 26 C 13.105 26 14 25.105 14 24 L 14 18 C 14 16.895 13.105 16 12 16 L 6 16 z M 18 16 C 16.895 16 16 16.895 16 18 L 16 24 C 16 25.105 16.895 26 18 26 L 24 26 C 25.105 26 26 25.105 26 24 L 26 18 C 26 16.895 25.105 16 24 16 L 18 16 z M 21 17.5 L 24.5 21 L 21 24.5 L 17.5 21 L 21 17.5 z M 9 18 L 11.886719 23 L 6.1132812 23 L 9 18 z" />
</svg>
);
}
function ProjectIcon(props: React.ComponentProps<'svg'>) {
return (
<svg width="40" height="40" viewBox="0 0 30 30" fill="currentcolor" {...props}>
<path d="M 14.984375 1.9863281 A 1.0001 1.0001 0 0 0 14 3 L 14 4 L 5 4 L 4 4 A 1.0001 1.0001 0 1 0 3.9804688 6 C 3.9348612 9.0608831 3.6893807 11.887023 3.1523438 14.142578 C 2.5565033 16.645108 1.6039585 18.395538 0.4453125 19.167969 A 1.0001 1.0001 0 0 0 1 21 L 4 21 C 4 22.105 4.895 23 6 23 L 11.787109 23 L 10.148438 26.042969 A 1.5 1.5 0 0 0 9 27.5 A 1.5 1.5 0 0 0 10.5 29 A 1.5 1.5 0 0 0 12 27.5 A 1.5 1.5 0 0 0 11.910156 26.992188 L 14.060547 23 L 15.939453 23 L 18.089844 26.992188 A 1.5 1.5 0 0 0 18 27.5 A 1.5 1.5 0 0 0 19.5 29 A 1.5 1.5 0 0 0 21 27.5 A 1.5 1.5 0 0 0 19.851562 26.042969 L 18.212891 23 L 24 23 C 25.105 23 26 22.105 26 21 L 26 6 A 1.0001 1.0001 0 1 0 26 4 L 25 4 L 16 4 L 16 3 A 1.0001 1.0001 0 0 0 14.984375 1.9863281 z M 5.9589844 6 L 14.832031 6 A 1.0001 1.0001 0 0 0 15.158203 6 L 23.958984 6 C 23.912194 9.0500505 23.687726 11.893974 23.152344 14.142578 C 22.583328 16.532444 21.674397 18.178754 20.585938 19 L 3.1523438 19 C 3.9976592 17.786874 4.6791735 16.365049 5.0976562 14.607422 C 5.6877248 12.129135 5.9137751 9.1554725 5.9589844 6 z" />
</svg>
);
}
function PersonIcon(props: React.ComponentProps<'svg'>) {
return (
<svg width="40" height="40" viewBox="0 0 30 30" fill="currentcolor" {...props}>
<path d="M15,3C8.373,3,3,8.373,3,15c0,6.627,5.373,12,12,12s12-5.373,12-12C27,8.373,21.627,3,15,3z M8,22.141 c1.167-3.5,4.667-2.134,5.25-4.03v-1.264c-0.262-0.141-1.013-1.109-1.092-1.865c-0.207-0.018-0.531-0.223-0.627-1.034 c-0.051-0.435,0.153-0.68,0.276-0.757c0,0-0.308-0.702-0.308-1.399C11.5,9.72,12.526,8,15,8c1.336,0,1.75,0.947,1.75,0.947 c1.194,0,1.75,1.309,1.75,2.844c0,0.765-0.308,1.399-0.308,1.399c0.124,0.077,0.328,0.322,0.277,0.757 c-0.096,0.811-0.42,1.016-0.627,1.034c-0.079,0.756-0.829,1.724-1.092,1.865v1.264c0.583,1.897,4.083,0.531,5.25,4.031 c0,0-2.618,2.859-7,2.859C10.593,25,8,22.141,8,22.141z" />
</svg>
);
}
```
## Anatomy
Import the component and assemble its parts:
```jsx title="Anatomy"
import { Tabs } from '@base-ui/react/tabs';
<Tabs.Root>
<Tabs.List>
<Tabs.Tab />
<Tabs.Indicator />
</Tabs.List>
<Tabs.Panel />
</Tabs.Root>;
```
## API reference
### Root
Groups the tabs and the corresponding panels.
Renders a `<div>` element.
**Root Props:**
| Prop | Type | Default | Description |
| :------------ | :--------------------------------------------------------------------------------------------------------------------------------------------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| defaultValue | `Tabs.Tab.Value` | `0` | The default value. Use when the component is not controlled.
When the value is `null`, no Tab will be active. |
| value | `Tabs.Tab.Value` | - | The value of the currently active `Tab`. Use when the component is controlled.
When the value is `null`, no Tab will be active. |
| onValueChange | `((value: Tabs.Tab.Value, eventDetails: Tabs.Root.ChangeEventDetails<'none', { activationDirection: Tabs.Tab.ActivationDirection }>) => void)` | - | Callback invoked when new value is being set. |
| orientation | `Tabs.Root.Orientation` | `'horizontal'` | The component orientation (layout flow direction). |
| className | `string \| ((state: Tabs.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Tabs.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Tabs.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Root Data Attributes:**
| Attribute | Type | Description |
| :------------------------ | :---------------------------------------------- | :---------------------------------------------------------------------------- |
| data-orientation | `'horizontal' \| 'vertical'` | Indicates the orientation of the tabs. |
| data-activation-direction | `'left' \| 'right' \| 'up' \| 'down' \| 'none'` | Indicates the direction of the activation (based on the previous active tab). |
### List
Groups the individual tab buttons.
Renders a `<div>` element.
**List Props:**
| Prop | Type | Default | Description |
| :-------------- | :----------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| activateOnFocus | `boolean` | `false` | Whether to automatically change the active tab on arrow key focus.
Otherwise, tabs will be activated using <kbd>Enter</kbd> or <kbd>Space</kbd> key press. |
| loopFocus | `boolean` | `true` | Whether to loop keyboard focus back to the first item
when the end of the list is reached while using the arrow keys. |
| className | `string \| ((state: Tabs.List.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Tabs.List.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Tabs.List.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**List Data Attributes:**
| Attribute | Type | Description |
| :------------------------ | :---------------------------------------------- | :---------------------------------------------------------------------------- |
| data-orientation | `'horizontal' \| 'vertical'` | Indicates the orientation of the tabs. |
| data-activation-direction | `'left' \| 'right' \| 'up' \| 'down' \| 'none'` | Indicates the direction of the activation (based on the previous active tab). |
### Tab
An individual interactive tab button that toggles the corresponding panel.
Renders a `<button>` element.
**Tab Props:**
| Prop | Type | Default | Description |
| :----------- | :---------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| value | `Tabs.Tab.Value` | - | The value of the Tab. |
| nativeButton | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it
via the `render` prop.
Set to `false` if the rendered element is not a button (e.g. `<div>`). |
| disabled | `boolean` | - | Whether the Tab is disabled.If a first Tab on a `<Tabs.List>` is disabled, it won't initially be selected.
Instead, the next enabled Tab will be selected.
However, it does not work like this during server-side rendering, as it is not known
during pre-rendering which Tabs are disabled.
To work around it, ensure that `defaultValue` or `value` on `<Tabs.Root>` is set to an enabled Tab's value. |
| className | `string \| ((state: Tabs.Tab.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Tabs.Tab.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Tabs.Tab.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Tab Data Attributes:**
| Attribute | Type | Description |
| :------------------------ | :---------------------------------------------- | :---------------------------------------------------------------------------- |
| data-orientation | `'horizontal' \| 'vertical'` | Indicates the orientation of the tabs. |
| data-disabled | - | Present when the tab is disabled. |
| data-activation-direction | `'left' \| 'right' \| 'up' \| 'down' \| 'none'` | Indicates the direction of the activation (based on the previous active tab). |
| data-active | - | Present when the tab is active. |
### Indicator
A visual indicator that can be styled to match the position of the currently active tab.
Renders a `<span>` element.
**Indicator Props:**
| Prop | Type | Default | Description |
| :-------------------- | :---------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| renderBeforeHydration | `boolean` | `false` | Whether to render itself before React hydrates.
This minimizes the time that the indicator isn’t visible after server-side rendering. |
| className | `string \| ((state: Tabs.Indicator.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Tabs.Indicator.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Tabs.Indicator.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Indicator Data Attributes:**
| Attribute | Type | Description |
| :------------------------ | :---------------------------------------------- | :---------------------------------------------------------------------------- |
| data-orientation | `'horizontal' \| 'vertical'` | Indicates the orientation of the tabs. |
| data-activation-direction | `'left' \| 'right' \| 'up' \| 'down' \| 'none'` | Indicates the direction of the activation (based on the previous active tab). |
**Indicator CSS Variables:**
| Variable | Type | Default | Description |
| :------------------ | :------- | :------ | :------------------------------------------------------------------------------------------ |
| --active-tab-bottom | `number` | - | Indicates the distance on the bottom side from the parent's container if the tab is active. |
| --active-tab-height | `number` | - | Indicates the width of the tab if it is active. |
| --active-tab-left | `number` | - | Indicates the distance on the left side from the parent's container if the tab is active. |
| --active-tab-right | `number` | - | Indicates the distance on the right side from the parent's container if the tab is active. |
| --active-tab-top | `number` | - | Indicates the distance on the top side from the parent's container if the tab is active. |
| --active-tab-width | `number` | - | Indicates the width of the tab if it is active. |
### Panel
A panel displayed when the corresponding tab is active.
Renders a `<div>` element.
**Panel Props:**
| Prop | Type | Default | Description |
| :---------- | :------------------------------------------------------------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| value | `Tabs.Tab.Value` | - | The value of the TabPanel. It will be shown when the Tab with the corresponding value is active. |
| className | `string \| ((state: Tabs.Panel.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Tabs.Panel.State) => CSSProperties \| undefined)` | - | - |
| keepMounted | `boolean` | `false` | Whether to keep the HTML element in the DOM while the panel is hidden. |
| render | `ReactElement \| ((props: HTMLProps, state: Tabs.Panel.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Panel Data Attributes:**
| Attribute | Type | Description |
| :------------------------ | :---------------------------------------------- | :---------------------------------------------------------------------------- |
| data-orientation | `'horizontal' \| 'vertical'` | Indicates the orientation of the tabs. |
| data-activation-direction | `'left' \| 'right' \| 'up' \| 'down' \| 'none'` | Indicates the direction of the activation (based on the previous active tab). |
| data-hidden | - | Present when the panel is hidden. |
| data-index | - | Indicates the index of the tab panel. |
references/toast.md
---
title: Toast
subtitle: Generates toast notifications.
description: A high-quality, unstyled React toast component to generate notifications.
---
# Toast
A high-quality, unstyled React toast component to generate notifications.
## v1.3.0 notes
- Toast manager flows can now close all toasts, which is useful when you want a global "dismiss notifications" action or need to clear stacked notices after route/context changes.
- If you add a bulk-dismiss UI, keep per-toast close affordances for keyboard and screen-reader users.
## v1.4.0 notes
- `toastManager.add()` supports upsert-style flows, which is useful when one logical notification should refresh instead of stacking duplicates.
- Touch swipe keeps the viewport expanded more reliably, and timers resume correctly after the window regains focus.
## v1.7.0 - v1.8.0 notes
- Toast updates can be provided as a functional updater derived from the current toast (for example self-referencing content or dynamic duration changes).
- The `render` prop is now honored in `Toast.Title`, `Toast.Description`, and `Toast.Action`, so you can swap or extend their default elements.
- Remaining timer calculation for paused/regained-focus toasts was fixed.
## v1.9.0 notes
- Timers stay paused during overlapping interactions, so a toast that is interacted with twice in a row does not resume its countdown early.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Toast } from "@base-ui/react/toast";
export default function ExampleToast() {
return (
<Toast.Provider>
<ToastButton />
<Toast.Portal>
<Toast.Viewport className="fixed z-10 top-auto right-[1rem] bottom-[1rem] mx-auto flex w-[250px] sm:right-[2rem] sm:bottom-[2rem] sm:w-[300px]">
<ToastList />
</Toast.Viewport>
</Toast.Portal>
</Toast.Provider>
);
}
function ToastButton() {
const toastManager = Toast.useToastManager();
const [count, setCount] = React.useState(0);
function createToast() {
setCount((prev) => prev + 1);
toastManager.add({
title: `Toast ${count + 1} created`,
description: "This is a toast notification.",
});
}
return (
<button type="button" className="box-border flex h-10 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3.5 py-0 font-medium text-gray-900 outline-0 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue active:bg-gray-100" onClick={createToast}>
Create toast
</button>
);
}
function ToastList() {
const { toasts } = Toast.useToastManager();
return toasts.map((toast) => (
<Toast.Root key={toast.id} toast={toast} className="[--gap:0.75rem] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] absolute right-0 bottom-0 left-auto z-[calc(1000-var(--toast-index))] mr-0 w-full origin-bottom [transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] rounded-lg border border-gray-200 bg-gray-50 bg-clip-padding p-4 shadow-lg select-none after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-[''] data-[ending-style]:opacity-0 data-[expanded]:[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--offset-y)))] data-[limited]:opacity-0 data-[starting-style]:[transform:translateY(150%)] [&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(150%)] data-[ending-style]:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))] data-[expanded]:data-[ending-style]:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))] data-[ending-style]:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-[expanded]:data-[ending-style]:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-[ending-style]:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-[expanded]:data-[ending-style]:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))] data-[expanded]:data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))] h-[var(--height)] data-[expanded]:h-[var(--toast-height)] [transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,height_0.15s]">
<Toast.Content className="overflow-hidden transition-opacity [transition-duration:250ms] data-[behind]:pointer-events-none data-[behind]:opacity-0 data-[expanded]:pointer-events-auto data-[expanded]:opacity-100">
<Toast.Title className="text-[0.975rem] leading-5 font-medium" />
<Toast.Description className="text-[0.925rem] leading-5 text-gray-700" />
<Toast.Close className="absolute top-2 right-2 flex h-5 w-5 items-center justify-center rounded border-none bg-transparent text-gray-500 hover:bg-gray-100 hover:text-gray-700" aria-label="Close">
<XIcon className="h-4 w-4" />
</Toast.Close>
</Toast.Content>
</Toast.Root>
));
}
function XIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Viewport {
position: fixed;
z-index: 1;
width: 250px;
margin: 0 auto;
bottom: 1rem;
right: 1rem;
left: auto;
top: auto;
@media (min-width: 500px) {
bottom: 2rem;
right: 2rem;
width: 300px;
}
}
.Toast {
--gap: 0.75rem;
--peek: 0.75rem;
--scale: calc(max(0, 1 - (var(--toast-index) * 0.1)));
--shrink: calc(1 - var(--scale));
--height: var(--toast-frontmost-height, var(--toast-height));
--offset-y: calc(var(--toast-offset-y) * -1 + (var(--toast-index) * var(--gap) * -1) + var(--toast-swipe-movement-y));
position: absolute;
right: 0;
margin: 0 auto;
box-sizing: border-box;
background: var(--color-gray-50);
color: var(--color-gray-900);
border: 1px solid var(--color-gray-200);
padding: 1rem;
width: 100%;
box-shadow: 0 2px 10px rgb(0 0 0 / 0.1);
background-clip: padding-box;
border-radius: 0.5rem;
transform-origin: bottom center;
bottom: 0;
left: auto;
margin-right: 0;
-webkit-user-select: none;
user-select: none;
transition:
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.5s,
height 0.15s;
cursor: default;
z-index: calc(1000 - var(--toast-index));
height: var(--height);
transform: translateX(var(--toast-swipe-movement-x)) translateY(calc(var(--toast-swipe-movement-y) - (var(--toast-index) * var(--peek)) - (var(--shrink) * var(--height)))) scale(var(--scale));
&[data-expanded] {
transform: translateX(var(--toast-swipe-movement-x)) translateY(var(--offset-y));
height: var(--toast-height);
}
&[data-starting-style],
&[data-ending-style] {
transform: translateY(150%);
}
&[data-limited] {
opacity: 0;
}
&[data-ending-style] {
opacity: 0;
&[data-swipe-direction="up"] {
transform: translateY(calc(var(--toast-swipe-movement-y) - 150%));
}
&[data-swipe-direction="left"] {
transform: translateX(calc(var(--toast-swipe-movement-x) - 150%)) translateY(var(--offset-y));
}
&[data-swipe-direction="right"] {
transform: translateX(calc(var(--toast-swipe-movement-x) + 150%)) translateY(var(--offset-y));
}
&[data-swipe-direction="down"] {
transform: translateY(calc(var(--toast-swipe-movement-y) + 150%));
}
}
&::after {
content: "";
position: absolute;
top: 100%;
width: 100%;
left: 0;
height: calc(var(--gap) + 1px);
}
}
.Content {
overflow: hidden;
transition: opacity 0.25s;
&[data-behind] {
opacity: 0;
}
&[data-expanded] {
opacity: 1;
}
}
.Title {
font-weight: 500;
font-size: 0.975rem;
line-height: 1.25rem;
margin: 0;
}
.Description {
font-size: 0.925rem;
line-height: 1.25rem;
margin: 0;
}
.Close {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0;
border: none;
background: transparent;
width: 1.25rem;
height: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
&:hover {
background-color: var(--color-gray-100);
}
}
.Icon {
width: 1rem;
height: 1rem;
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Toast } from "@base-ui/react/toast";
import styles from "./index.module.css";
export default function ExampleToast() {
return (
<Toast.Provider>
<ToastButton />
<Toast.Portal>
<Toast.Viewport className={styles.Viewport}>
<ToastList />
</Toast.Viewport>
</Toast.Portal>
</Toast.Provider>
);
}
function ToastButton() {
const toastManager = Toast.useToastManager();
const [count, setCount] = React.useState(0);
function createToast() {
setCount((prev) => prev + 1);
toastManager.add({
title: `Toast ${count + 1} created`,
description: "This is a toast notification.",
});
}
return (
<button type="button" className={styles.Button} onClick={createToast}>
Create toast
</button>
);
}
function ToastList() {
const { toasts } = Toast.useToastManager();
return toasts.map((toast) => (
<Toast.Root key={toast.id} toast={toast} className={styles.Toast}>
<Toast.Content className={styles.Content}>
<Toast.Title className={styles.Title} />
<Toast.Description className={styles.Description} />
<Toast.Close className={styles.Close} aria-label="Close">
<XIcon className={styles.Icon} />
</Toast.Close>
</Toast.Content>
</Toast.Root>
));
}
function XIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
);
}
```
## Anatomy
Import the component and assemble its parts:
```jsx title="Anatomy"
import { Toast } from "@base-ui/react/toast";
<Toast.Provider>
<Toast.Portal>
<Toast.Viewport>
{/* Stacked toasts */}
<Toast.Root>
<Toast.Content>
<Toast.Title />
<Toast.Description />
<Toast.Action />
<Toast.Close />
</Toast.Content>
</Toast.Root>
{/* Anchored toasts */}
<Toast.Positioner>
<Toast.Root>
<Toast.Arrow />
<Toast.Content>
<Toast.Title />
<Toast.Description />
<Toast.Action />
<Toast.Close />
</Toast.Content>
</Toast.Root>
</Toast.Positioner>
</Toast.Viewport>
</Toast.Portal>
</Toast.Provider>;
```
## General usage
- `<Toast.Provider>` can be wrapped around your entire app, ensuring all toasts are rendered in the same viewport.
- <kbd>F6</kbd> lets users jump into the toast viewport landmark region to navigate toasts with
keyboard focus.
- The `data-swipe-ignore` attribute can be manually added to elements inside of a toast to prevent swipe-to-dismiss gestures on them. Interactive elements are automatically prevented.
## Global manager
A global toast manager can be created by passing the `toastManager` prop to the `<Toast.Provider>`.
This enables you to queue a toast from anywhere in the app (such as in functions outside the React tree) while still using the same toast renderer.
The created `toastManager` object has the same properties and methods as the `Toast.useToastManager()` hook.
```tsx title="Creating a manager instance"
const toastManager = Toast.createToastManager();
```
```jsx title="Using the instance"
<Toast.Provider toastManager={toastManager}>
```
## Stacking and animations
The `--toast-index` CSS variable can be used to determine the stacking order of the toasts.
The 0th index toast appears at the front.
```css title="z-index stacking"
.Toast {
z-index: calc(1000 - var(--toast-index));
transform: scale(1 - calc(0.1 * var(--toast-index)));
}
```
The `--toast-offset-y` CSS variable can be used to determine the vertical offset of the toasts when positioned absolutely with a translation offset — this is usually used with the `data-expanded` attribute, present when the toast viewport is being hovered or has focus.
```css title="Expanded offset"
.Toast[data-expanded] {
transform: translateY(var(--toast-offset-y));
}
```
`<Toast.Content>` is used to hide overflow from taller toasts while the stack is collapsed.
The `data-behind` attribute marks content that sits behind the frontmost toast and pairs with the `data-expanded` attribute so the content fades back in when the viewport expands:
```css title="Collapsed content" "data-behind" "data-expanded"
.ToastContent {
overflow: hidden;
transition: opacity 0.25s;
}
.ToastContent[data-behind] {
opacity: 0;
}
.ToastContent[data-expanded] {
opacity: 1;
}
```
The `--toast-swipe-movement-x` and `--toast-swipe-movement-y` CSS variables are used to determine the swipe movement of the toasts in order to add a translation offset.
```css title="Swipe offset" "--toast-swipe-movement-x" "--toast-swipe-movement-y"
.Toast {
transform: scale(1 - calc(0.1 * var(--toast-index))) translateX(var(--toast-swipe-movement-x)) translateY(calc(var(--toast-swipe-movement-y) + (var(--toast-index) * -20%)));
}
```
The `data-swipe-direction` attribute can be used to determine the swipe direction of the toasts to add a translation offset upon dismissal.
```css title="Swipe direction" "data-swipe-direction"
&[data-ending-style] {
opacity: 0;
&[data-swipe-direction="up"] {
transform: translateY(calc(var(--toast-swipe-movement-y) - 150%));
}
&[data-swipe-direction="down"] {
transform: translateY(calc(var(--toast-swipe-movement-y) + 150%));
}
/* Note: --offset-y is defined locally in these examples and derives from
--toast-offset-y, --toast-index, and swipe movement values */
&[data-swipe-direction="left"] {
transform: translateX(calc(var(--toast-swipe-movement-x) - 150%)) translateY(var(--offset-y));
}
&[data-swipe-direction="right"] {
transform: translateX(calc(var(--toast-swipe-movement-x) + 150%)) translateY(var(--offset-y));
}
}
```
The `data-limited` attribute indicates that the toast was removed from the list due to exceeding the `limit` option.
This is useful for animating the toast differently when it is removed from the list.
## Examples
### Anchored toasts
Toasts can be anchored to a specific element using `<Toast.Positioner>` and the `positionerProps` option when adding a toast. This is useful for showing contextual feedback like transient "Copied" toasts that appear near the button that triggered the action.
Anchored toasts should be rendered in a separate `<Toast.Provider>` from stacked toasts. A global toast manager can be created for each to manage them separately throughout your app:
```tsx title="Mixing stacked and anchored toasts"
const anchoredToastManager = Toast.createToastManager();
const stackedToastManager = Toast.createToastManager();
function App() {
return (
<React.Fragment>
<Toast.Provider toastManager={anchoredToastManager}>
<AnchoredToasts />
</Toast.Provider>
<Toast.Provider toastManager={stackedToastManager}>
<StackedToasts />
</Toast.Provider>
{/* App content */}
</React.Fragment>
);
}
function AnchoredToasts() {
const { toasts } = Toast.useToastManager();
return (
<Toast.Portal>
<Toast.Viewport>
{toasts.map((toast) => (
<Toast.Positioner key={toast.id} toast={toast}>
<Toast.Root toast={toast}>{/* ... */}</Toast.Root>
</Toast.Positioner>
))}
</Toast.Viewport>
</Toast.Portal>
);
}
function StackedToasts() {
const { toasts } = Toast.useToastManager();
return (
<Toast.Portal>
<Toast.Viewport>
{toasts.map((toast) => (
<Toast.Root key={toast.id} toast={toast}>
{/* ... */}
</Toast.Root>
))}
</Toast.Viewport>
</Toast.Portal>
);
}
```
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Toast } from "@base-ui/react/toast";
import { Button } from "@base-ui/react/button";
import { Tooltip } from "@base-ui/react/tooltip";
const stackedToastManager = Toast.createToastManager();
const anchoredToastManager = Toast.createToastManager();
export default function ExampleToast() {
return (
<Tooltip.Provider>
<Toast.Provider toastManager={anchoredToastManager}>
<AnchoredToasts />
</Toast.Provider>
<Toast.Provider toastManager={stackedToastManager}>
<StackedToasts />
</Toast.Provider>
<div className="flex items-center gap-2">
<CopyButton />
<StackedToastButton />
</div>
</Tooltip.Provider>
);
}
function StackedToastButton() {
function createToast() {
stackedToastManager.add({
description: "Copied",
});
}
return (
<button type="button" className="box-border flex h-10 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3.5 py-0 font-medium text-gray-900 outline-0 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100" onClick={createToast}>
Stacked toast
</button>
);
}
function CopyButton() {
const [copied, setCopied] = React.useState(false);
const buttonRef = React.useRef<HTMLButtonElement | null>(null);
function handleCopy() {
setCopied(true);
anchoredToastManager.add({
description: "Copied",
positionerProps: {
anchor: buttonRef.current,
sideOffset: 8,
},
timeout: 1500,
onClose() {
setCopied(false);
},
});
}
return (
<Tooltip.Root
disabled={copied}
onOpenChange={(open, eventDetails) => {
if (eventDetails.reason === "trigger-press") {
eventDetails.cancel();
}
}}
>
<Tooltip.Trigger ref={buttonRef} className="box-border flex h-10 w-10 items-center justify-center rounded-md border border-gray-200 bg-gray-50 text-gray-900 outline-0 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100" onClick={handleCopy} aria-label="Copy to clipboard" render={<Button disabled={copied} focusableWhenDisabled />}>
{copied ? <CheckIcon className="h-5 w-5" /> : <ClipboardIcon className="h-5 w-5" />}
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner sideOffset={8}>
<Tooltip.Popup className="flex origin-(--transform-origin) flex-col rounded-md bg-[canvas] px-2 py-1 text-sm shadow-lg shadow-gray-200 outline-1 outline-gray-200 transition-[transform,scale,opacity] data-ending-style:scale-90 data-ending-style:opacity-0 data-instant:duration-0 data-starting-style:scale-90 data-starting-style:opacity-0 dark:shadow-none dark:-outline-offset-1 dark:outline-gray-300">
<Tooltip.Arrow className="data-[side=bottom]:-top-2 data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:-bottom-2 data-[side=top]:rotate-180">
<ArrowSvg />
</Tooltip.Arrow>
Copy
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
</Tooltip.Root>
);
}
function AnchoredToasts() {
const { toasts } = Toast.useToastManager();
return (
<Toast.Portal>
<Toast.Viewport className="outline-0">
{toasts.map((toast) => (
<Toast.Positioner key={toast.id} toast={toast} className="z-[calc(1000-var(--toast-index))]">
<Toast.Root toast={toast} className="group flex w-max origin-(--transform-origin) flex-col rounded-md bg-[canvas] px-2 py-1 text-sm shadow-lg shadow-gray-200 outline-1 outline-gray-200 transition-[transform,scale,opacity] data-ending-style:scale-90 data-ending-style:opacity-0 data-starting-style:scale-90 data-starting-style:opacity-0 focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 dark:shadow-none dark:-outline-offset-1 dark:outline-gray-300 dark:focus-visible:outline-blue-400">
<Toast.Arrow className="data-[side=bottom]:-top-2 data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:-bottom-2 data-[side=top]:rotate-180">
<ArrowSvg />
</Toast.Arrow>
<Toast.Content>
<Toast.Description />
</Toast.Content>
</Toast.Root>
</Toast.Positioner>
))}
</Toast.Viewport>
</Toast.Portal>
);
}
function StackedToasts() {
const { toasts } = Toast.useToastManager();
return (
<Toast.Portal>
<Toast.Viewport className="fixed z-10 top-auto right-[1rem] bottom-[1rem] mx-auto flex w-[250px] sm:right-[2rem] sm:bottom-[2rem] sm:w-[300px]">
{toasts.map((toast) => (
<Toast.Root key={toast.id} toast={toast} className="[--gap:0.75rem] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] absolute right-0 bottom-0 left-auto z-[calc(1000-var(--toast-index))] mr-0 w-full origin-bottom [transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] rounded-lg border border-gray-200 bg-gray-50 bg-clip-padding p-4 shadow-lg select-none after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-[''] data-[ending-style]:opacity-0 data-[expanded]:[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--offset-y)))] data-[limited]:opacity-0 data-[starting-style]:[transform:translateY(150%)] [&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(150%)] data-[ending-style]:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))] data-[expanded]:data-[ending-style]:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))] data-[ending-style]:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-[expanded]:data-[ending-style]:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-[ending-style]:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-[expanded]:data-[ending-style]:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))] data-[expanded]:data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))] h-[var(--height)] data-[expanded]:h-[var(--toast-height)] [transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,height_0.15s]">
<Toast.Content className="overflow-hidden transition-opacity [transition-duration:250ms] data-[behind]:pointer-events-none data-[behind]:opacity-0 data-[expanded]:pointer-events-auto data-[expanded]:opacity-100">
<Toast.Title className="text-[0.975rem] leading-5 font-medium" />
<Toast.Description className="text-[0.925rem] leading-5 text-gray-700" />
<Toast.Close className="absolute top-2 right-2 flex h-5 w-5 items-center justify-center rounded border-none bg-transparent text-gray-500 hover:bg-gray-100 hover:text-gray-700" aria-label="Close">
<XIcon className="h-4 w-4" />
</Toast.Close>
</Toast.Content>
</Toast.Root>
))}
</Toast.Viewport>
</Toast.Portal>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className="fill-[canvas]" />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className="fill-gray-200 dark:fill-none" />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className="dark:fill-gray-300" />
</svg>
);
}
function ClipboardIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
<rect width="8" height="4" x="8" y="2" rx="1" ry="1" />
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" />
</svg>
);
}
function CheckIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M20 6 9 17l-5-5" />
</svg>
);
}
function XIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.ButtonGroup {
display: flex;
gap: 0.5rem;
align-items: center;
}
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.CopyButton {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
padding: 0;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.AnchoredViewport {
outline: 0;
}
.AnchoredPositioner {
z-index: calc(1000 - var(--toast-index));
}
.AnchoredToast {
box-sizing: border-box;
font-size: 0.875rem;
line-height: 1.25rem;
display: flex;
flex-direction: column;
width: max-content;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
background-color: canvas;
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
&:focus-visible {
outline: 1px solid var(--color-blue-800);
outline-offset: -1px;
}
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
&:focus-visible {
outline: 1px solid var(--color-blue-400);
outline-offset: -1px;
}
}
}
.StackedViewport {
position: fixed;
z-index: 1;
width: 250px;
margin: 0 auto;
bottom: 1rem;
right: 1rem;
left: auto;
top: auto;
@media (min-width: 500px) {
bottom: 2rem;
right: 2rem;
width: 300px;
}
}
.StackedToast {
--gap: 0.75rem;
--peek: 0.75rem;
--scale: calc(max(0, 1 - (var(--toast-index) * 0.1)));
--shrink: calc(1 - var(--scale));
--height: var(--toast-frontmost-height, var(--toast-height));
--offset-y: calc(var(--toast-offset-y) * -1 + (var(--toast-index) * var(--gap) * -1) + var(--toast-swipe-movement-y));
position: absolute;
right: 0;
margin: 0 auto;
box-sizing: border-box;
background: var(--color-gray-50);
color: var(--color-gray-900);
border: 1px solid var(--color-gray-200);
padding: 1rem;
width: 100%;
box-shadow: 0 2px 10px rgb(0 0 0 / 0.1);
background-clip: padding-box;
border-radius: 0.5rem;
transform-origin: bottom center;
bottom: 0;
left: auto;
margin-right: 0;
-webkit-user-select: none;
user-select: none;
transition:
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.5s,
height 0.15s;
cursor: default;
z-index: calc(1000 - var(--toast-index));
height: var(--height);
transform: translateX(var(--toast-swipe-movement-x)) translateY(calc(var(--toast-swipe-movement-y) - (var(--toast-index) * var(--peek)) - (var(--shrink) * var(--height)))) scale(var(--scale));
&[data-expanded] {
transform: translateX(var(--toast-swipe-movement-x)) translateY(var(--offset-y));
height: var(--toast-height);
}
&[data-starting-style],
&[data-ending-style] {
transform: translateY(150%);
}
&[data-limited] {
opacity: 0;
}
&[data-ending-style] {
opacity: 0;
&[data-swipe-direction="up"] {
transform: translateY(calc(var(--toast-swipe-movement-y) - 150%));
}
&[data-swipe-direction="left"] {
transform: translateX(calc(var(--toast-swipe-movement-x) - 150%)) translateY(var(--offset-y));
}
&[data-swipe-direction="right"] {
transform: translateX(calc(var(--toast-swipe-movement-x) + 150%)) translateY(var(--offset-y));
}
&[data-swipe-direction="down"] {
transform: translateY(calc(var(--toast-swipe-movement-y) + 150%));
}
}
&::after {
content: "";
position: absolute;
top: 100%;
width: 100%;
left: 0;
height: calc(var(--gap) + 1px);
}
}
.Content {
overflow: hidden;
transition: opacity 0.25s;
&[data-behind] {
opacity: 0;
}
&[data-expanded] {
opacity: 1;
}
}
.Title {
font-weight: 500;
font-size: 0.975rem;
line-height: 1.25rem;
margin: 0;
}
.Description {
font-size: 0.925rem;
line-height: 1.25rem;
margin: 0;
}
.Close {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0;
border: none;
background: transparent;
width: 1.25rem;
height: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
&:hover {
background-color: var(--color-gray-100);
}
}
.Icon {
width: 1rem;
height: 1rem;
}
.Tooltip {
box-sizing: border-box;
font-size: 0.875rem;
line-height: 1.25rem;
display: flex;
flex-direction: column;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
background-color: canvas;
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
&[data-instant] {
transition-duration: 0ms;
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.Viewport {
outline: 0;
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Toast } from "@base-ui/react/toast";
import { Button } from "@base-ui/react/button";
import { Tooltip } from "@base-ui/react/tooltip";
import styles from "./index.module.css";
const anchoredToastManager = Toast.createToastManager();
const stackedToastManager = Toast.createToastManager();
export default function ExampleToast() {
return (
<Tooltip.Provider>
<Toast.Provider toastManager={anchoredToastManager}>
<AnchoredToasts />
</Toast.Provider>
<Toast.Provider toastManager={stackedToastManager}>
<StackedToasts />
</Toast.Provider>
<div className={styles.ButtonGroup}>
<CopyButton />
<StackedToastButton />
</div>
</Tooltip.Provider>
);
}
function StackedToastButton() {
function createToast() {
stackedToastManager.add({
description: "Copied",
});
}
return (
<button type="button" className={styles.Button} onClick={createToast}>
Stacked toast
</button>
);
}
function CopyButton() {
const [copied, setCopied] = React.useState(false);
const buttonRef = React.useRef<HTMLButtonElement | null>(null);
function handleCopy() {
setCopied(true);
anchoredToastManager.add({
description: "Copied",
positionerProps: {
anchor: buttonRef.current,
sideOffset: 8,
},
timeout: 1500,
onClose() {
setCopied(false);
},
});
}
return (
<Tooltip.Root
disabled={copied}
onOpenChange={(open, eventDetails) => {
if (eventDetails.reason === "trigger-press") {
eventDetails.cancel();
}
}}
>
<Tooltip.Trigger ref={buttonRef} className={styles.CopyButton} onClick={handleCopy} aria-label="Copy to clipboard" render={<Button disabled={copied} focusableWhenDisabled />}>
{copied ? <CheckIcon className={styles.Icon} /> : <ClipboardIcon className={styles.Icon} />}
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner sideOffset={8}>
<Tooltip.Popup className={styles.Tooltip}>
<Tooltip.Arrow className={styles.Arrow}>
<ArrowSvg />
</Tooltip.Arrow>
Copy
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
</Tooltip.Root>
);
}
function AnchoredToasts() {
const { toasts } = Toast.useToastManager();
return (
<Toast.Portal>
<Toast.Viewport className={styles.AnchoredViewport}>
{toasts.map((toast) => (
<Toast.Positioner key={toast.id} toast={toast} className={styles.AnchoredPositioner}>
<Toast.Root toast={toast} className={styles.AnchoredToast}>
<Toast.Arrow className={styles.Arrow}>
<ArrowSvg />
</Toast.Arrow>
<Toast.Content>
<Toast.Description />
</Toast.Content>
</Toast.Root>
</Toast.Positioner>
))}
</Toast.Viewport>
</Toast.Portal>
);
}
function StackedToasts() {
const { toasts } = Toast.useToastManager();
return (
<Toast.Portal>
<Toast.Viewport className={styles.StackedViewport}>
{toasts.map((toast) => (
<Toast.Root key={toast.id} toast={toast} className={styles.StackedToast}>
<Toast.Content className={styles.Content}>
<Toast.Title className={styles.Title} />
<Toast.Description className={styles.Description} />
<Toast.Close className={styles.Close} aria-label="Close">
<XIcon className={styles.Icon} />
</Toast.Close>
</Toast.Content>
</Toast.Root>
))}
</Toast.Viewport>
</Toast.Portal>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className={styles.ArrowFill} />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className={styles.ArrowOuterStroke} />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className={styles.ArrowInnerStroke} />
</svg>
);
}
function ClipboardIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
<rect width="8" height="4" x="8" y="2" rx="1" ry="1" />
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" />
</svg>
);
}
function CheckIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M20 6 9 17l-5-5" />
</svg>
);
}
function XIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
);
}
```
### Custom position
The position of the toasts is controlled by your own CSS.
To change the toasts' position, you can modify the `.Viewport` and `.Root` styles.
A more general component could accept a `data-position` attribute, which the CSS handles for each variation.
The following shows a top-center position:
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Toast } from "@base-ui/react/toast";
export default function ExampleToast() {
return (
<Toast.Provider>
<ToastButton />
<Toast.Portal>
<Toast.Viewport className="fixed z-10 top-[1rem] right-0 bottom-auto left-0 mx-auto flex w-full max-w-[300px]">
<ToastList />
</Toast.Viewport>
</Toast.Portal>
</Toast.Provider>
);
}
function ToastButton() {
const toastManager = Toast.useToastManager();
const [count, setCount] = React.useState(0);
function createToast() {
setCount((prev) => prev + 1);
toastManager.add({
title: `Toast ${count + 1} created`,
description: "This is a toast notification.",
});
}
return (
<button type="button" className="box-border flex h-10 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3.5 py-0 font-medium text-gray-900 outline-0 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue active:bg-gray-100" onClick={createToast}>
Create toast
</button>
);
}
function ToastList() {
const { toasts } = Toast.useToastManager();
return toasts.map((toast) => (
<Toast.Root key={toast.id} toast={toast} swipeDirection="up" className="[--gap:0.75rem] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)+(var(--toast-index)*var(--gap))+var(--toast-swipe-movement-y))] absolute right-0 top-0 left-0 z-[calc(1000-var(--toast-index))] mx-auto w-[300px] origin-top [transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--peek))+(var(--shrink)*var(--height))))_scale(var(--scale))] rounded-lg border border-gray-200 bg-gray-50 bg-clip-padding p-4 shadow-lg select-none after:absolute after:bottom-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-[''] data-[ending-style]:opacity-0 data-[expanded]:[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--offset-y)))] data-[limited]:opacity-0 data-[starting-style]:[transform:translateY(-150%)] [&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(-150%)] data-[ending-style]:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))] data-[expanded]:data-[ending-style]:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))] data-[ending-style]:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-[expanded]:data-[ending-style]:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-[ending-style]:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-[expanded]:data-[ending-style]:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))] data-[expanded]:data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))] h-[var(--height)] data-[expanded]:h-[var(--toast-height)] [transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,height_0.15s]">
<Toast.Content className="overflow-hidden transition-opacity [transition-duration:250ms] data-[behind]:pointer-events-none data-[behind]:opacity-0 data-[expanded]:pointer-events-auto data-[expanded]:opacity-100">
<Toast.Title className="text-[0.975rem] leading-5 font-medium" />
<Toast.Description className="text-[0.925rem] leading-5 text-gray-700" />
<Toast.Close className="absolute top-2 right-2 flex h-5 w-5 items-center justify-center rounded border-none bg-transparent text-gray-500 hover:bg-gray-100 hover:text-gray-700" aria-label="Close">
<XIcon className="h-4 w-4" />
</Toast.Close>
</Toast.Content>
</Toast.Root>
));
}
function XIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Viewport {
position: fixed;
z-index: 1;
width: 100%;
max-width: 300px;
margin: 0 auto;
top: 1rem;
right: 0;
left: 0;
bottom: auto;
}
.Toast {
--gap: 0.75rem;
--peek: 0.75rem;
--scale: calc(max(0, 1 - (var(--toast-index) * 0.1)));
--shrink: calc(1 - var(--scale));
--height: var(--toast-frontmost-height, var(--toast-height));
--offset-y: calc(var(--toast-offset-y) + (var(--toast-index) * var(--gap)) + var(--toast-swipe-movement-y));
position: absolute;
margin: 0 auto;
box-sizing: border-box;
background: var(--color-gray-50);
color: var(--color-gray-900);
border: 1px solid var(--color-gray-200);
padding: 1rem;
width: 100%;
box-shadow: 0 2px 10px rgb(0 0 0 / 0.1);
background-clip: padding-box;
border-radius: 0.5rem;
transform-origin: top center;
top: 0;
left: 0;
right: 0;
margin-right: auto;
margin-left: auto;
-webkit-user-select: none;
user-select: none;
transition:
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.5s,
height 0.15s;
cursor: default;
z-index: calc(1000 - var(--toast-index));
height: var(--height);
transform: translateX(var(--toast-swipe-movement-x)) translateY(calc(var(--toast-swipe-movement-y) + (var(--toast-index) * var(--peek)) + (var(--shrink) * var(--height)))) scale(var(--scale));
&[data-expanded] {
transform: translateX(var(--toast-swipe-movement-x)) translateY(var(--offset-y));
height: var(--toast-height);
}
&[data-starting-style],
&[data-ending-style] {
transform: translateY(-150%);
}
&[data-limited] {
opacity: 0;
}
&[data-ending-style] {
opacity: 0;
&[data-swipe-direction="up"] {
transform: translateY(calc(var(--toast-swipe-movement-y) - 150%));
}
&[data-swipe-direction="left"] {
transform: translateX(calc(var(--toast-swipe-movement-x) - 150%)) translateY(var(--offset-y));
}
&[data-swipe-direction="right"] {
transform: translateX(calc(var(--toast-swipe-movement-x) + 150%)) translateY(var(--offset-y));
}
&[data-swipe-direction="down"] {
transform: translateY(calc(var(--toast-swipe-movement-y) + 150%));
}
}
&::after {
content: "";
position: absolute;
width: 100%;
bottom: 100%;
left: 0;
height: calc(var(--gap) + 1px);
}
}
.Content {
overflow: hidden;
transition: opacity 0.25s;
&[data-behind] {
opacity: 0;
}
&[data-expanded] {
opacity: 1;
}
}
.Title {
font-weight: 500;
font-size: 0.975rem;
line-height: 1.25rem;
margin: 0;
}
.Description {
font-size: 0.925rem;
line-height: 1.25rem;
margin: 0;
}
.Close {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0;
border: none;
background: transparent;
width: 1.25rem;
height: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
&:hover {
background-color: var(--color-gray-100);
}
}
.Icon {
width: 1rem;
height: 1rem;
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Toast } from "@base-ui/react/toast";
import styles from "./index.module.css";
export default function ExampleToast() {
return (
<Toast.Provider>
<ToastButton />
<Toast.Portal>
<Toast.Viewport className={styles.Viewport}>
<ToastList />
</Toast.Viewport>
</Toast.Portal>
</Toast.Provider>
);
}
function ToastButton() {
const toastManager = Toast.useToastManager();
const [count, setCount] = React.useState(0);
function createToast() {
setCount((prev) => prev + 1);
toastManager.add({
title: `Toast ${count + 1} created`,
description: "This is a toast notification.",
});
}
return (
<button type="button" className={styles.Button} onClick={createToast}>
Create toast
</button>
);
}
function ToastList() {
const { toasts } = Toast.useToastManager();
return toasts.map((toast) => (
<Toast.Root key={toast.id} toast={toast} swipeDirection="up" className={styles.Toast}>
<Toast.Content className={styles.Content}>
<Toast.Title className={styles.Title} />
<Toast.Description className={styles.Description} />
<Toast.Close className={styles.Close} aria-label="Close">
<XIcon className={styles.Icon} />
</Toast.Close>
</Toast.Content>
</Toast.Root>
));
}
function XIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
);
}
```
### Undo action
When adding a toast, the `actionProps` option can be used to define props for an action button inside of it—this enables the ability to undo an action associated with the toast.
## Demo
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Viewport {
position: fixed;
z-index: 1;
width: 275px;
margin: 0 auto;
bottom: 1rem;
right: 1rem;
left: auto;
top: auto;
@media (min-width: 500px) {
bottom: 2rem;
right: 2rem;
width: 300px;
}
}
.Toast {
--gap: 0.75rem;
--peek: 0.75rem;
--scale: calc(max(0, 1 - (var(--toast-index) * 0.1)));
--shrink: calc(1 - var(--scale));
--height: var(--toast-frontmost-height, var(--toast-height));
--offset-y: calc(var(--toast-offset-y) * -1 + (var(--toast-index) * var(--gap) * -1) + var(--toast-swipe-movement-y));
position: absolute;
right: 0;
margin: 0 auto;
box-sizing: border-box;
background: var(--color-gray-50);
color: var(--color-gray-900);
border: 1px solid var(--color-gray-200);
padding: 1rem;
width: 100%;
box-shadow: 0 2px 10px rgb(0 0 0 / 0.1);
background-clip: padding-box;
border-radius: 0.5rem;
transform-origin: bottom center;
bottom: 0;
left: auto;
margin-right: 0;
-webkit-user-select: none;
user-select: none;
transition:
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.5s,
height 0.15s;
cursor: default;
z-index: calc(1000 - var(--toast-index));
height: var(--height);
transform: translateX(var(--toast-swipe-movement-x)) translateY(calc(var(--toast-swipe-movement-y) - (var(--toast-index) * var(--peek)) - (var(--shrink) * var(--height)))) scale(var(--scale));
&[data-expanded] {
transform: translateX(var(--toast-swipe-movement-x)) translateY(var(--offset-y));
height: var(--toast-height);
}
&[data-starting-style],
&[data-ending-style] {
transform: translateY(150%);
}
&[data-limited] {
opacity: 0;
}
&[data-ending-style] {
opacity: 0;
&[data-swipe-direction="up"] {
transform: translateY(calc(var(--toast-swipe-movement-y) - 150%));
}
&[data-swipe-direction="left"] {
transform: translateX(calc(var(--toast-swipe-movement-x) - 150%)) translateY(var(--offset-y));
}
&[data-swipe-direction="right"] {
transform: translateX(calc(var(--toast-swipe-movement-x) + 150%)) translateY(var(--offset-y));
}
&[data-swipe-direction="down"] {
transform: translateY(calc(var(--toast-swipe-movement-y) + 150%));
}
}
&::after {
content: "";
position: absolute;
width: 100%;
top: 100%;
left: 0;
height: calc(var(--gap) + 1px);
}
}
.Content {
overflow: hidden;
transition: opacity 0.25s;
&[data-behind] {
opacity: 0;
}
&[data-expanded] {
opacity: 1;
}
}
.Title {
font-weight: 500;
font-size: 0.975rem;
line-height: 1.25rem;
margin: 0;
}
.Description {
font-size: 0.925rem;
line-height: 1.25rem;
margin: 0;
}
.UndoButton {
display: inline-flex;
align-items: center;
justify-content: center;
height: 2rem;
padding: 0 0.75rem;
font-size: 0.875rem;
font-weight: 500;
line-height: 1.25rem;
border-radius: 0.25rem;
margin-top: 0.5rem;
background-color: var(--color-gray-900);
color: var(--color-gray-50);
border: none;
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Close {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0;
border: none;
background: transparent;
width: 1.25rem;
height: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
&:hover {
background-color: var(--color-gray-100);
}
}
.Icon {
width: 1rem;
height: 1rem;
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Toast } from "@base-ui/react/toast";
import styles from "./index.module.css";
export default function UndoToastExample() {
return (
<Toast.Provider>
<Form />
<Toast.Portal>
<Toast.Viewport className={styles.Viewport}>
<ToastList />
</Toast.Viewport>
</Toast.Portal>
</Toast.Provider>
);
}
function Form() {
const toastManager = Toast.useToastManager();
function action() {
const id = toastManager.add({
title: "Action performed",
description: "You can undo this action.",
type: "success",
actionProps: {
children: "Undo",
onClick() {
toastManager.close(id);
toastManager.add({
title: "Action undone",
});
},
},
});
}
return (
<button type="button" onClick={action} className={styles.Button}>
Perform action
</button>
);
}
function ToastList() {
const { toasts } = Toast.useToastManager();
return toasts.map((toast) => (
<Toast.Root key={toast.id} toast={toast} className={styles.Toast}>
<Toast.Content className={styles.Content}>
<Toast.Title className={styles.Title} />
<Toast.Description className={styles.Description} />
<Toast.Action className={styles.UndoButton} />
<Toast.Close className={styles.Close} aria-label="Close">
<XIcon className={styles.Icon} />
</Toast.Close>
</Toast.Content>
</Toast.Root>
));
}
function XIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
);
}
```
### Promise
An asynchronous toast can be created with three possible states: `loading`, `success`, and `error`.
The `type` string matches these states to change the styling.
Each of the states also accepts the [method options](/react/components/toast.md) object for more granular control.
## Demo
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Viewport {
position: fixed;
z-index: 1;
width: 250px;
margin: 0 auto;
bottom: 1rem;
right: 1rem;
left: auto;
top: auto;
@media (min-width: 500px) {
bottom: 2rem;
right: 2rem;
width: 300px;
}
}
.Toast {
--gap: 0.75rem;
--peek: 0.75rem;
--scale: calc(max(0, 1 - (var(--toast-index) * 0.1)));
--shrink: calc(1 - var(--scale));
--height: var(--toast-frontmost-height, var(--toast-height));
--offset-y: calc(var(--toast-offset-y) * -1 + (var(--toast-index) * var(--gap) * -1) + var(--toast-swipe-movement-y));
position: absolute;
right: 0;
margin: 0 auto;
box-sizing: border-box;
background: var(--color-gray-50);
color: var(--color-gray-900);
border: 1px solid var(--color-gray-200);
padding: 1rem;
width: 100%;
box-shadow: 0 2px 10px rgb(0 0 0 / 0.1);
background-clip: padding-box;
border-radius: 0.5rem;
transform-origin: bottom center;
bottom: 0;
left: auto;
margin-right: 0;
-webkit-user-select: none;
user-select: none;
transition:
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.5s,
height 0.15s;
cursor: default;
z-index: calc(1000 - var(--toast-index));
height: var(--height);
transform: translateX(var(--toast-swipe-movement-x)) translateY(calc(var(--toast-swipe-movement-y) - (var(--toast-index) * var(--peek)) - (var(--shrink) * var(--height)))) scale(var(--scale));
&[data-expanded] {
transform: translateX(var(--toast-swipe-movement-x)) translateY(var(--offset-y));
height: var(--toast-height);
}
&[data-starting-style],
&[data-ending-style] {
transform: translateY(150%);
}
&[data-limited] {
opacity: 0;
}
&[data-ending-style] {
opacity: 0;
&[data-swipe-direction="up"] {
transform: translateY(calc(var(--toast-swipe-movement-y) - 150%));
}
&[data-swipe-direction="left"] {
transform: translateX(calc(var(--toast-swipe-movement-x) - 150%)) translateY(var(--offset-y));
}
&[data-swipe-direction="right"] {
transform: translateX(calc(var(--toast-swipe-movement-x) + 150%)) translateY(var(--offset-y));
}
&[data-swipe-direction="down"] {
transform: translateY(calc(var(--toast-swipe-movement-y) + 150%));
}
}
&::after {
content: "";
position: absolute;
width: 100%;
top: 100%;
left: 0;
height: calc(var(--gap) + 1px);
}
&[data-type="success"] {
background-color: lightgreen;
color: black;
}
&[data-type="error"] {
background-color: lightpink;
color: black;
}
}
.Content {
overflow: hidden;
transition: opacity 0.25s;
&[data-behind] {
opacity: 0;
}
&[data-expanded] {
opacity: 1;
}
}
.Title {
font-weight: 500;
font-size: 0.975rem;
line-height: 1.25rem;
margin: 0;
}
.Description {
font-size: 0.925rem;
line-height: 1.25rem;
margin: 0;
}
.Close {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0;
border: none;
background: transparent;
width: 1.25rem;
height: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
&:hover {
background-color: var(--color-gray-100);
}
}
.Icon {
width: 1rem;
height: 1rem;
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Toast } from "@base-ui/react/toast";
import styles from "./index.module.css";
export default function PromiseToastExample() {
return (
<Toast.Provider>
<PromiseDemo />
<Toast.Portal>
<Toast.Viewport className={styles.Viewport}>
<ToastList />
</Toast.Viewport>
</Toast.Portal>
</Toast.Provider>
);
}
function PromiseDemo() {
const toastManager = Toast.useToastManager();
function runPromise() {
toastManager.promise(
// Simulate an API request with a promise that resolves after 2 seconds
new Promise<string>((resolve, reject) => {
const shouldSucceed = Math.random() > 0.3; // 70% success rate
setTimeout(() => {
if (shouldSucceed) {
resolve("operation completed");
} else {
reject(new Error("operation failed"));
}
}, 2000);
}),
{
loading: "Loading data…",
success: (data: string) => `Success: ${data}`,
error: (err: Error) => `Error: ${err.message}`,
},
);
}
return (
<button type="button" onClick={runPromise} className={styles.Button}>
Run promise
</button>
);
}
function ToastList() {
const { toasts } = Toast.useToastManager();
return toasts.map((toast) => (
<Toast.Root key={toast.id} toast={toast} className={styles.Toast}>
<Toast.Content className={styles.Content}>
<Toast.Title className={styles.Title} />
<Toast.Description className={styles.Description} />
<Toast.Close className={styles.Close} aria-label="Close">
<XIcon className={styles.Icon} />
</Toast.Close>
</Toast.Content>
</Toast.Root>
));
}
function XIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
);
}
```
### Custom
A toast with custom data can be created by passing any typed object interface to the `data` option.
This enables you to pass any data (including functions) you need to the toast and access it in the toast's rendering logic.
## Demo
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Viewport {
position: fixed;
z-index: 1;
width: 250px;
margin: 0 auto;
bottom: 1rem;
right: 1rem;
left: auto;
top: auto;
@media (min-width: 500px) {
bottom: 2rem;
right: 2rem;
width: 300px;
}
}
.Toast {
--gap: 0.75rem;
--peek: 0.75rem;
--scale: calc(max(0, 1 - (var(--toast-index) * 0.1)));
--shrink: calc(1 - var(--scale));
--height: var(--toast-frontmost-height, var(--toast-height));
--offset-y: calc(var(--toast-offset-y) * -1 + (var(--toast-index) * var(--gap) * -1) + var(--toast-swipe-movement-y));
position: absolute;
right: 0;
margin: 0 auto;
box-sizing: border-box;
background: var(--color-gray-50);
color: var(--color-gray-900);
border: 1px solid var(--color-gray-200);
padding: 1rem;
width: 100%;
box-shadow: 0 2px 10px rgb(0 0 0 / 0.1);
background-clip: padding-box;
border-radius: 0.5rem;
transform-origin: bottom center;
bottom: 0;
left: auto;
margin-right: 0;
-webkit-user-select: none;
user-select: none;
transition:
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.5s,
height 0.15s;
cursor: default;
z-index: calc(1000 - var(--toast-index));
height: var(--height);
transform: translateX(var(--toast-swipe-movement-x)) translateY(calc(var(--toast-swipe-movement-y) - (var(--toast-index) * var(--peek)) - (var(--shrink) * var(--height)))) scale(var(--scale));
&[data-expanded] {
transform: translateX(var(--toast-swipe-movement-x)) translateY(var(--offset-y));
height: var(--toast-height);
}
&[data-starting-style],
&[data-ending-style] {
transform: translateY(150%);
}
&[data-limited] {
opacity: 0;
}
&[data-ending-style] {
opacity: 0;
&[data-swipe-direction="up"] {
transform: translateY(calc(var(--toast-swipe-movement-y) - 150%));
}
&[data-swipe-direction="left"] {
transform: translateX(calc(var(--toast-swipe-movement-x) - 150%)) translateY(var(--offset-y));
}
&[data-swipe-direction="right"] {
transform: translateX(calc(var(--toast-swipe-movement-x) + 150%)) translateY(var(--offset-y));
}
&[data-swipe-direction="down"] {
transform: translateY(calc(var(--toast-swipe-movement-y) + 150%));
}
}
&::after {
content: "";
position: absolute;
top: 100%;
width: 100%;
left: 0;
height: calc(var(--gap) + 1px);
}
}
.Content {
overflow: hidden;
transition: opacity 0.25s cubic-bezier(0.22, 1, 0.36, 1);
&[data-behind] {
opacity: 0;
}
&[data-expanded] {
opacity: 1;
}
}
.Title {
font-weight: 500;
font-size: 0.975rem;
line-height: 1.25rem;
margin: 0;
}
.Description {
font-size: 0.925rem;
line-height: 1.25rem;
margin: 0;
}
.Close {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0;
border: none;
background: transparent;
width: 1.25rem;
height: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
&:hover {
background-color: var(--color-gray-100);
}
}
.Icon {
width: 1rem;
height: 1rem;
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Toast } from "@base-ui/react/toast";
import styles from "./index.module.css";
interface CustomToastData {
userId: string;
}
function isCustomToast(toast: Toast.Root.ToastObject): toast is Toast.Root.ToastObject<CustomToastData> {
return toast.data?.userId !== undefined;
}
export default function CustomToastExample() {
return (
<Toast.Provider>
<CustomToast />
<Toast.Portal>
<Toast.Viewport className={styles.Viewport}>
<ToastList />
</Toast.Viewport>
</Toast.Portal>
</Toast.Provider>
);
}
function CustomToast() {
const toastManager = Toast.useToastManager();
function action() {
const data: CustomToastData = {
userId: "123",
};
toastManager.add({
title: "Toast with custom data",
data,
});
}
return (
<button type="button" onClick={action} className={styles.Button}>
Create custom toast
</button>
);
}
function ToastList() {
const { toasts } = Toast.useToastManager();
return toasts.map((toast) => (
<Toast.Root key={toast.id} toast={toast} className={styles.Toast}>
<Toast.Content className={styles.Content}>
<Toast.Title className={styles.Title}>{toast.title}</Toast.Title>
{isCustomToast(toast) && toast.data ? <Toast.Description className={styles.Description}>`data.userId` is {toast.data.userId}</Toast.Description> : <Toast.Description className={styles.Description} />}
<Toast.Close className={styles.Close} aria-label="Close">
<XIcon className={styles.Icon} />
</Toast.Close>
</Toast.Content>
</Toast.Root>
));
}
function XIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
);
}
```
### Varying heights
Toasts with varying heights are stacked by ensuring that the `<Toast.Content>` element has `overflow: hidden` set, along with all toasts' heights matching the frontmost toast at index 0.
This prevents taller toasts from overflowing the stack when collapsed.
## Demo
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Viewport {
position: fixed;
z-index: 1;
width: 250px;
margin: 0 auto;
bottom: 1rem;
right: 1rem;
left: auto;
top: auto;
@media (min-width: 500px) {
bottom: 2rem;
right: 2rem;
width: 300px;
}
}
.Toast {
--gap: 0.75rem;
--peek: 0.75rem;
--scale: calc(max(0, 1 - (var(--toast-index) * 0.1)));
--shrink: calc(1 - var(--scale));
--height: var(--toast-frontmost-height, var(--toast-height));
--offset-y: calc(var(--toast-offset-y) * -1 + (var(--toast-index) * var(--gap) * -1) + var(--toast-swipe-movement-y));
position: absolute;
right: 0;
margin: 0 auto;
box-sizing: border-box;
background: var(--color-gray-50);
color: var(--color-gray-900);
border: 1px solid var(--color-gray-200);
padding: 1rem;
width: 100%;
box-shadow: 0 2px 10px rgb(0 0 0 / 0.1);
background-clip: padding-box;
border-radius: 0.5rem;
bottom: 0;
left: auto;
margin-right: 0;
-webkit-user-select: none;
user-select: none;
transition:
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.5s,
height 0.15s;
cursor: default;
z-index: calc(1000 - var(--toast-index));
height: var(--height);
transform-origin: bottom center;
transform: translateX(var(--toast-swipe-movement-x)) translateY(calc(var(--toast-swipe-movement-y) - (var(--toast-index) * var(--peek)) - (var(--shrink) * var(--height)))) scale(var(--scale));
&[data-expanded] {
transform: translateX(var(--toast-swipe-movement-x)) translateY(var(--offset-y));
height: var(--toast-height);
}
&[data-starting-style],
&[data-ending-style] {
transform: translateY(150%);
}
&[data-limited] {
opacity: 0;
}
&[data-ending-style] {
opacity: 0;
&[data-swipe-direction="up"] {
transform: translateY(calc(var(--toast-swipe-movement-y) - 150%));
}
&[data-swipe-direction="left"] {
transform: translateX(calc(var(--toast-swipe-movement-x) - 150%)) translateY(var(--offset-y));
}
&[data-swipe-direction="right"] {
transform: translateX(calc(var(--toast-swipe-movement-x) + 150%)) translateY(var(--offset-y));
}
&[data-swipe-direction="down"] {
transform: translateY(calc(var(--toast-swipe-movement-y) + 150%));
}
}
&::after {
content: "";
position: absolute;
width: 100%;
top: 100%;
left: 0;
height: calc(var(--gap) + 1px);
}
}
.Content {
overflow: hidden;
transition: opacity 0.25s;
&[data-behind] {
opacity: 0;
}
&[data-expanded] {
opacity: 1;
}
}
.Title {
font-weight: 500;
font-size: 0.975rem;
line-height: 1.25rem;
margin: 0;
}
.Description {
font-size: 0.925rem;
line-height: 1.25rem;
margin: 0;
}
.Close {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0;
border: none;
background: transparent;
width: 1.25rem;
height: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
&:hover {
background-color: var(--color-gray-100);
}
}
.Icon {
width: 1rem;
height: 1rem;
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Toast } from "@base-ui/react/toast";
import styles from "./index.module.css";
export default function VaryingHeightsToast() {
return (
<Toast.Provider>
<ToastButton />
<Toast.Portal>
<Toast.Viewport className={styles.Viewport}>
<ToastList />
</Toast.Viewport>
</Toast.Portal>
</Toast.Provider>
);
}
function ToastButton() {
const toastManager = Toast.useToastManager();
const [count, setCount] = React.useState(0);
function createToast() {
setCount((prev) => prev + 1);
const description = TEXTS[Math.floor(Math.random() * TEXTS.length)];
toastManager.add({
title: `Toast ${count + 1} created`,
description,
});
}
return (
<button type="button" className={styles.Button} onClick={createToast}>
Create varying height toast
</button>
);
}
function ToastList() {
const { toasts } = Toast.useToastManager();
return toasts.map((toast) => (
<Toast.Root key={toast.id} toast={toast} className={styles.Toast}>
<Toast.Content className={styles.Content}>
<Toast.Title className={styles.Title} />
<Toast.Description className={styles.Description} />
<Toast.Close className={styles.Close} aria-label="Close">
<XIcon className={styles.Icon} />
</Toast.Close>
</Toast.Content>
</Toast.Root>
));
}
function XIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
);
}
const TEXTS = ["Short message.", "A bit longer message that spans two lines.", "This is a longer description that intentionally takes more vertical space to demonstrate stacking with varying heights.", "An even longer description that should span multiple lines so we can verify the clamped collapsed height and smooth expansion animation when hovering or focusing the viewport."];
```
## API reference
### Provider
Provides a context for creating and managing toasts.
**Provider Props:**
| Prop | Type | Default | Description |
| :----------- | :------------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------- |
| limit | `number` | `3` | The maximum number of toasts that can be displayed at once.
When the limit is reached, the oldest toast will be removed to make room for the new one. |
| toastManager | `ToastManager` | - | A global manager for toasts to use outside of a React component. |
| timeout | `number` | `5000` | The default amount of time (in ms) before a toast is auto dismissed.
A value of `0` will prevent the toast from being dismissed automatically. |
| children | `ReactNode` | - | - |
### Viewport
A container viewport for toasts.
Renders a `<div>` element.
**Viewport Props:**
| Prop | Type | Default | Description |
| :-------- | :---------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Toast.Viewport.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Toast.Viewport.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Toast.Viewport.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Viewport Data Attributes:**
| Attribute | Type | Description |
| :------------ | :-------- | :--------------------------------------------- |
| data-expanded | `boolean` | Indicates toasts are expanded in the viewport. |
**Viewport CSS Variables:**
| Variable | Type | Default | Description |
| :----------------------- | :------- | :------ | :------------------------------------------- |
| --toast-frontmost-height | `number` | - | Indicates the height of the frontmost toast. |
### Portal
A portal element that moves the viewport to a different part of the DOM.
By default, the portal element is appended to `<body>`.
Renders a `<div>` element.
**Portal Props:**
| Prop | Type | Default | Description |
| :-------- | :---------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| container | `HTMLElement \| ShadowRoot \| RefObject<HTMLElement \| ShadowRoot \| null> \| null` | - | A parent element to render the portal element into. |
| className | `string \| ((state: any) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: any) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: any) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Root
Groups all parts of an individual toast.
Renders a `<div>` element.
**Root Props:**
| Prop | Type | Default | Description |
| :------------- | :------------------------------------------------------------------------------- | :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| swipeDirection | `'left' \| 'right' \| 'up' \| 'down' \| ('left' \| 'right' \| 'up' \| 'down')[]` | `['down', 'right']` | Direction(s) in which the toast can be swiped to dismiss. |
| toast | `Toast.Root.ToastObject<any>` | - | The toast to render. |
| className | `string \| ((state: Toast.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Toast.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Toast.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Root Data Attributes:**
| Attribute | Type | Description |
| :------------------- | :------------------------------------ | :------------------------------------------------------------- |
| data-expanded | `boolean` | Present when the toast is expanded in the viewport. |
| data-limited | `boolean` | Present when the toast was removed due to exceeding the limit. |
| data-swipe-direction | `'up' \| 'down' \| 'left' \| 'right'` | The direction the toast was swiped. |
| data-swiping | `boolean` | Present when the toast is being swiped. |
| data-type | `string` | The type of the toast. |
| data-starting-style | - | Present when the toast is animating in. |
| data-ending-style | - | Present when the toast is animating out. |
**Root CSS Variables:**
| Variable | Type | Default | Description |
| :----------------------- | :------- | :------ | :--------------------------------------------------------------------------- |
| --toast-height | `number` | - | Indicates the measured natural height of the toast in pixels. |
| --toast-index | `number` | - | Indicates the index of the toast in the list. |
| --toast-offset-y | `number` | - | Indicates the vertical pixels offset of the toast in the list when expanded. |
| --toast-swipe-movement-x | `number` | - | Indicates the horizontal swipe movement of the toast. |
| --toast-swipe-movement-y | `number` | - | Indicates the vertical swipe movement of the toast. |
### Content
A container for the contents of a toast.
Renders a `<div>` element.
**Content Props:**
| Prop | Type | Default | Description |
| :-------- | :--------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Toast.Content.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Toast.Content.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Toast.Content.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Content Data Attributes:**
| Attribute | Type | Description |
| :------------ | :-------- | :----------------------------------------------------------------- |
| data-behind | `boolean` | Present when the toast is behind the frontmost toast in the stack. |
| data-expanded | `boolean` | Present when the toast viewport is expanded. |
### Title
A title that labels the toast.
Renders an `<h2>` element.
**Title Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Toast.Title.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Toast.Title.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Toast.Title.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Title Data Attributes:**
| Attribute | Type | Description |
| :-------- | :------- | :--------------------- |
| data-type | `string` | The type of the toast. |
### Description
A description that describes the toast.
Can be used as the default message for the toast when no title is provided.
Renders a `<p>` element.
**Description Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Toast.Description.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Toast.Description.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Toast.Description.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Description Data Attributes:**
| Attribute | Type | Description |
| :-------- | :------- | :--------------------- |
| data-type | `string` | The type of the toast. |
### Action
Performs an action when clicked.
Renders a `<button>` element.
**Action Props:**
| Prop | Type | Default | Description |
| :----------- | :-------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| nativeButton | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it
via the `render` prop.
Set to `false` if the rendered element is not a button (e.g. `<div>`). |
| className | `string \| ((state: Toast.Action.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Toast.Action.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Toast.Action.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Action Data Attributes:**
| Attribute | Type | Description |
| :-------- | :------- | :--------------------- |
| data-type | `string` | The type of the toast. |
### Close
Closes the toast when clicked.
Renders a `<button>` element.
**Close Props:**
| Prop | Type | Default | Description |
| :----------- | :------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| nativeButton | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it
via the `render` prop.
Set to `false` if the rendered element is not a button (e.g. `<div>`). |
| className | `string \| ((state: Toast.Close.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Toast.Close.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Toast.Close.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Close Data Attributes:**
| Attribute | Type | Description |
| :-------- | :------- | :--------------------- |
| data-type | `string` | The type of the toast. |
### Positioner
Positions the toast against the anchor.
Renders a `<div>` element.
**Positioner Props:**
| Prop | Type | Default | Description |
| :-------------------- | :------------------------- | :--------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| disableAnchorTracking | `boolean` | `false` | Whether to disable the popup from tracking any layout shift of its positioning anchor. |
| toast | `ToastObject<any>` | - | The toast object associated with the positioner. |
| align | `Align` | `'center'` | How to align the popup relative to the specified side. |
| alignOffset | `number \| OffsetFunction` | `0` | Additional offset along the alignment axis in pixels.
Also accepts a function that returns the offset to read the dimensions of the anchor
and positioner elements, along with its side and alignment.The function takes a `data` object parameter with the following properties:\* `data.anchor`: the dimensions of the anchor element with properties `width` and `height`. |
- `data.positioner`: the dimensions of the positioner element with properties `width` and `height`.
- `data.side`: which side of the anchor element the positioner is aligned against.
- `data.align`: how the positioner is aligned relative to the specified side. |
| side | `Side` | `'top'` | Which side of the anchor element to align the toast against.
May automatically change to avoid collisions. |
| sideOffset | `number \| OffsetFunction` | `0` | Distance between the anchor and the popup in pixels.
Also accepts a function that returns the distance to read the dimensions of the anchor
and positioner elements, along with its side and alignment.The function takes a `data` object parameter with the following properties:\* `data.anchor`: the dimensions of the anchor element with properties `width` and `height`.
- `data.positioner`: the dimensions of the positioner element with properties `width` and `height`.
- `data.side`: which side of the anchor element the positioner is aligned against.
- `data.align`: how the positioner is aligned relative to the specified side. |
| arrowPadding | `number` | `5` | Minimum distance to maintain between the arrow and the edges of the popup.Use it to prevent the arrow element from hanging out of the rounded corners of a popup. |
| anchor | `Element \| null` | - | An element to position the toast against. |
| collisionAvoidance | `CollisionAvoidance` | - | Determines how to handle collisions when positioning the popup. |
| collisionBoundary | `Boundary` | `'clipping-ancestors'` | An element or a rectangle that delimits the area that the popup is confined to. |
| collisionPadding | `Padding` | `5` | Additional space to maintain from the edge of the collision boundary. |
| sticky | `boolean` | `false` | Whether to maintain the popup in the viewport after
the anchor element was scrolled out of view. |
| positionMethod | `'fixed' \| 'absolute'` | `'absolute'` | Determines which CSS `position` property to use. |
| className | `string \| ((state: Toast.Positioner.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Toast.Positioner.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Toast.Positioner.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Positioner Data Attributes:**
| Attribute | Type | Description |
| :----------------- | :------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| data-anchor-hidden | - | Present when the anchor is hidden. |
| data-align | `'start' \| 'center' \| 'end'` | Indicates how the toast is aligned relative to specified side. |
| data-side | `'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the toast is positioned relative to the trigger. |
**Positioner CSS Variables:**
| Variable | Type | Default | Description |
| :----------------- | :------- | :------ | :------------------------------------------------------------------------------------- |
| --anchor-height | `number` | - | The anchor's height. |
| --anchor-width | `number` | - | The anchor's width. |
| --available-height | `number` | - | The available height between the anchor and the edge of the viewport. |
| --available-width | `number` | - | The available width between the anchor and the edge of the viewport. |
| --transform-origin | `string` | - | The coordinates that this element is anchored to. Used for animations and transitions. |
### Arrow
Displays an element positioned against the toast anchor.
Renders a `<div>` element.
**Arrow Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Toast.Arrow.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Toast.Arrow.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Toast.Arrow.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Arrow Data Attributes:**
| Attribute | Type | Description |
| :-------------- | :------------------------------------------------------------------------- | :------------------------------------------------------------------- |
| data-uncentered | - | Present when the toast arrow is uncentered. |
| data-align | `'start' \| 'center' \| 'end'` | Indicates how the toast is aligned relative to specified side. |
| data-side | `'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the toast is positioned relative to the anchor. |
## useToastManager
Manages toasts, called inside of a `<Toast.Provider>`.
```tsx title="Usage"
const toastManager = Toast.useToastManager();
```
### Return value
**Return Value:**
| Property | Type | Description |
| :------- | :---------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------- |
| toasts | `Toast.Root.ToastObject[]` | The array of toast objects. |
| add | `(options: ToastManagerAddOptions) => string` | Add a toast to the toast list. |
| close | `(toastId: string) => void` | Closes and removes a toast from the toast list. |
| update | `(toastId: string, options: ToastManagerUpdateOptions) => void` | Update a toast in the toast list. |
| promise | `<Value>(promise: Promise<Value>, options: ToastManagerPromiseOptions) => Promise<Value>` | Create a toast that resolves with a value, with three possible states for the toast: `loading`, `success`, and `error`. |
### Method options
**Props:**
| Prop | Type | Default | Description |
| :---------- | :---------------- | :------ | :----------------------------------------------------------------------------------------- |
| title | `React.ReactNode` | - | The title of the toast. |
| description | `React.ReactNode` | - | The description of the toast. |
| type | `string` | - | The type of the toast. Used to conditionally style the toast or render different elements. |
| timeout | `number` | - | The amount of time (in ms) before the toast is auto dismissed. |
| priority | `'low' \| 'high'` | `'low'` | ``` |
The priority of the toast.
- \`low\` - The toast will be announced politely.
- \`high\` - The toast will be announced urgently.
````|
| onClose | `() => void` | - | A callback invoked when the toast is closed. |
| onRemove | `() => void` | - | A callback invoked when the toast is removed from the list after animations complete when closed. |
| actionProps | `React.ComponentPropsWithRef<'button'>` | - | The props of the action button. |
| data | `Record<string, unknown>` | - | The data of the toast. |
### `add` method
Creates a toast by adding it to the toast list.
Returns a `toastId` that can be used to update or close the toast later.
```jsx title="Usage"
const toastId = toastManager.add({
description: 'Hello, world!',
});
````
```jsx title="Example" {2,7-9}
function App() {
const toastManager = Toast.useToastManager();
return (
<button
type="button"
onClick={() => {
toastManager.add({
description: "Hello, world!",
});
}}
>
Add toast
</button>
);
}
```
For high priority toasts, the `title` and `description` strings are what are used to announce the toast to screen readers.
Screen readers do not announce any extra content rendered inside `<Toast.Root>`, including the `<Toast.Title>` or `<Toast.Description>` components, unless they intentionally navigate to the toast viewport.
### `update` method
Updates the toast with new options.
```jsx title="Usage"
toastManager.update(toastId, {
description: "New description",
});
```
### `close` method
Closes the toast, removing it from the toast list after any animations complete.
```jsx title="Usage"
toastManager.close(toastId);
```
### `promise` method
Creates an asynchronous toast with three possible states: `loading`, `success`, and `error`.
```tsx title="Description configuration"
const promise = toastManager.promise(
new Promise((resolve) => {
setTimeout(() => resolve("world!"), 1000);
}),
{
// Each are a shortcut for the `description` option
loading: "Loading…",
success: (data) => `Hello ${data}`,
error: (err) => `Error: ${err}`,
},
);
```
Each state also accepts the [method options](/react/components/toast.md) object to granularly control the toast for each state:
```tsx title="Method options configuration"
const promise = toastManager.promise(
new Promise((resolve) => {
setTimeout(() => resolve("world!"), 1000);
}),
{
loading: {
title: "Loading…",
description: "The promise is loading.",
},
success: {
title: "Success",
description: "The promise resolved successfully.",
},
error: {
title: "Error",
description: "The promise rejected.",
actionProps: {
children: "Contact support",
onClick() {
// Redirect to support page
},
},
},
},
);
```
references/toggle-group.md
---
title: Toggle Group
subtitle: Provides a shared state to a series of toggle buttons.
description: A high-quality, unstyled React toggle group component that provides shared state to a series of toggle buttons.
---
# Toggle Group
A high-quality, unstyled React toggle group component that provides shared state to a series of toggle buttons.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import * as React from "react";
import { Toggle } from "@base-ui/react/toggle";
import { ToggleGroup } from "@base-ui/react/toggle-group";
export default function ExampleToggleGroup() {
return (
<ToggleGroup defaultValue={["left"]} className="flex gap-px rounded-md border border-gray-200 bg-gray-50 p-0.5">
<Toggle aria-label="Align left" value="left" className="flex size-8 items-center justify-center rounded-sm text-gray-600 select-none hover:bg-gray-100 focus-visible:bg-none focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-200 data-[pressed]:bg-gray-100 data-[pressed]:text-gray-900">
<AlignLeftIcon className="size-4" />
</Toggle>
<Toggle aria-label="Align center" value="center" className="flex size-8 items-center justify-center rounded-sm text-gray-600 select-none hover:bg-gray-100 focus-visible:bg-none focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-200 data-[pressed]:bg-gray-100 data-[pressed]:text-gray-900">
<AlignCenterIcon className="size-4" />
</Toggle>
<Toggle aria-label="Align right" value="right" className="flex size-8 items-center justify-center rounded-sm text-gray-600 select-none hover:bg-gray-100 focus-visible:bg-none focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-200 data-[pressed]:bg-gray-100 data-[pressed]:text-gray-900">
<AlignRightIcon className="size-4" />
</Toggle>
</ToggleGroup>
);
}
function AlignLeftIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="16" height="16" viewBox="0 0 16 16" stroke="currentColor" strokeLinecap="round" {...props}>
<path d="M2.5 3.5H13.5" />
<path d="M2.5 9.5H13.5" />
<path d="M2.5 6.5H10.5" />
<path d="M2.5 12.5H10.5" />
</svg>
);
}
function AlignCenterIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="16" height="16" viewBox="0 0 16 16" stroke="currentColor" strokeLinecap="round" {...props}>
<path d="M3 3.5H14" />
<path d="M3 9.5H14" />
<path d="M4.5 6.5H12.5" />
<path d="M4.5 12.5H12.5" />
</svg>
);
}
function AlignRightIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="16" height="16" viewBox="0 0 16 16" stroke="currentColor" strokeLinecap="round" {...props}>
<path d="M2.5 3.5H13.5" />
<path d="M2.5 9.5H13.5" />
<path d="M5.5 6.5H13.5" />
<path d="M5.5 12.5H13.5" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Panel {
display: flex;
gap: 1px;
border: 1px solid var(--color-gray-200);
background-color: var(--color-gray-50);
border-radius: 0.375rem;
padding: 0.125rem;
}
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
padding: 0;
margin: 0;
outline: 0;
border: 0;
border-radius: 0.25rem;
background-color: transparent;
color: var(--color-gray-600);
user-select: none;
&:focus-visible {
background-color: transparent;
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-200);
}
&[data-pressed] {
background-color: var(--color-gray-100);
color: var(--color-gray-900);
}
}
.Icon {
width: 1rem;
height: 1rem;
}
```
```tsx
/* index.tsx */
import * as React from "react";
import { Toggle } from "@base-ui/react/toggle";
import { ToggleGroup } from "@base-ui/react/toggle-group";
import styles from "./index.module.css";
export default function ExampleToggleGroup() {
return (
<ToggleGroup defaultValue={["left"]} className={styles.Panel}>
<Toggle aria-label="Align left" value="left" className={styles.Button}>
<AlignLeftIcon className={styles.Icon} />
</Toggle>
<Toggle aria-label="Align center" value="center" className={styles.Button}>
<AlignCenterIcon className={styles.Icon} />
</Toggle>
<Toggle aria-label="Align right" value="right" className={styles.Button}>
<AlignRightIcon className={styles.Icon} />
</Toggle>
</ToggleGroup>
);
}
function AlignLeftIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="16" height="16" viewBox="0 0 16 16" stroke="currentcolor" strokeLinecap="round" {...props}>
<path d="M2.5 3.5H13.5" />
<path d="M2.5 9.5H13.5" />
<path d="M2.5 6.5H10.5" />
<path d="M2.5 12.5H10.5" />
</svg>
);
}
function AlignCenterIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="16" height="16" viewBox="0 0 16 16" stroke="currentcolor" strokeLinecap="round" {...props}>
<path d="M3 3.5H14" />
<path d="M3 9.5H14" />
<path d="M4.5 6.5H12.5" />
<path d="M4.5 12.5H12.5" />
</svg>
);
}
function AlignRightIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="16" height="16" viewBox="0 0 16 16" stroke="currentcolor" strokeLinecap="round" {...props}>
<path d="M2.5 3.5H13.5" />
<path d="M2.5 9.5H13.5" />
<path d="M5.5 6.5H13.5" />
<path d="M5.5 12.5H13.5" />
</svg>
);
}
```
## Anatomy
Import the component and use it as a single part:
```jsx title="Anatomy"
import { ToggleGroup } from "@base-ui/react/toggle-group";
<ToggleGroup />;
```
## API reference
Provides a shared state to a series of toggle buttons.
**ToggleGroup Props:**
| Prop | Type | Default | Description |
| :------------ | :------------------------------------------------------------------------------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| defaultValue | `any[]` | - | The pressed state of the toggle group represented by an array of
the values of all pressed toggle buttons.
This is the uncontrolled counterpart of `value`. |
| value | `any[]` | - | The pressed state of the toggle group represented by an array of
the values of all pressed toggle buttons.
This is the controlled counterpart of `defaultValue`. |
| onValueChange | `((groupValue: any[], eventDetails: Toggle.Group.ChangeEventDetails) => void)` | - | Callback fired when the pressed states of the toggle group changes. |
| loopFocus | `boolean` | `true` | Whether to loop keyboard focus back to the first item
when the end of the list is reached while using the arrow keys. |
| multiple | `boolean` | `false` | When `false` only one item in the group can be pressed. If any item in
the group becomes pressed, the others will become unpressed.
When `true` multiple items can be pressed. |
| disabled | `boolean` | `false` | Whether the toggle group should ignore user interaction. |
| orientation | `Orientation` | `'horizontal'` | - |
| className | `string \| ((state: ToggleGroup.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: ToggleGroup.State) => CSSProperties \| undefined)` | - | Inline styles applied to the root element, or a function that returns styles based on `ToggleGroup.State`. Returning `undefined` applies no inline styles. |
| render | `ReactElement \| ((props: HTMLProps, state: ToggleGroup.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**ToggleGroup Data Attributes:**
| Attribute | Type | Description |
| :--------------- | :--------------------------- | :------------------------------------------------------------------------------------------------- |
| data-orientation | `'horizontal' \| 'vertical'` | Indicates the orientation of the toggle group. |
| data-disabled | - | Present when the toggle group is disabled. |
| data-multiple | - | Present when the toggle group allows multiple buttons to be in the pressed state at the same time. |
references/toggle.md
---
title: Toggle
subtitle: A two-state button that can be on or off.
description: A high-quality, unstyled React toggle component that displays a two-state button that can be on or off.
---
# Toggle
A high-quality, unstyled React toggle component that displays a two-state button that can be on or off.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
'use client';
import * as React from 'react';
import { Toggle } from '@base-ui/react/toggle';
export default function ExampleToggle() {
return (
<div className="flex gap-px rounded-md border border-gray-200 bg-gray-50 p-0.5">
<Toggle
aria-label="Favorite"
className="flex size-8 items-center justify-center rounded-sm text-gray-600 select-none hover:bg-gray-100 focus-visible:bg-none focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-200 data-[pressed]:text-gray-900"
render={(props, state) => {
if (state.pressed) {
return (
<button type="button" {...props}>
<HeartFilledIcon className="size-5" />
</button>
);
}
return (
<button type="button" {...props}>
<HeartOutlineIcon className="size-5" />
</button>
);
}}
/>
</div>
);
}
function HeartFilledIcon(props: React.ComponentProps<'svg'>) {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentcolor" {...props}>
<path d="M7.99961 13.8667C7.88761 13.8667 7.77561 13.8315 7.68121 13.7611C7.43321 13.5766 1.59961 9.1963 1.59961 5.8667C1.59961 3.80856 3.27481 2.13336 5.33294 2.13336C6.59054 2.13336 7.49934 2.81176 7.99961 3.3131C8.49988 2.81176 9.40868 2.13336 10.6663 2.13336C12.7244 2.13336 14.3996 3.80803 14.3996 5.8667C14.3996 9.1963 8.56601 13.5766 8.31801 13.7616C8.22361 13.8315 8.11161 13.8667 7.99961 13.8667Z" />
</svg>
);
}
function HeartOutlineIcon(props: React.ComponentProps<'svg'>) {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentcolor" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.99961 4.8232L7.24456 4.06654C6.84123 3.66235 6.18866 3.20003 5.33294 3.20003C3.86391 3.20003 2.66628 4.39767 2.66628 5.8667C2.66628 6.4079 2.91276 7.1023 3.41967 7.91383C3.91548 8.70759 4.59649 9.51244 5.31278 10.2503C6.38267 11.3525 7.47318 12.2465 7.99983 12.6605C8.52734 12.2456 9.61718 11.352 10.6864 10.2504C11.4027 9.51248 12.0837 8.70762 12.5796 7.91384C13.0865 7.1023 13.3329 6.4079 13.3329 5.8667C13.3329 4.39723 12.1354 3.20003 10.6663 3.20003C9.81056 3.20003 9.15799 3.66235 8.75466 4.06654L7.99961 4.8232ZM7.98574 3.29926C7.48264 2.79938 6.57901 2.13336 5.33294 2.13336C3.27481 2.13336 1.59961 3.80856 1.59961 5.8667C1.59961 9.1963 7.43321 13.5766 7.68121 13.7611C7.77561 13.8315 7.88761 13.8667 7.99961 13.8667C8.11161 13.8667 8.22361 13.8315 8.31801 13.7616C8.56601 13.5766 14.3996 9.1963 14.3996 5.8667C14.3996 3.80803 12.7244 2.13336 10.6663 2.13336C9.42013 2.13336 8.51645 2.79947 8.01337 3.29936C8.00877 3.30393 8.00421 3.30849 7.99967 3.31303C7.99965 3.31305 7.99963 3.31307 7.99961 3.3131C7.99502 3.3085 7.9904 3.30389 7.98574 3.29926Z"
/>
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Panel {
display: flex;
gap: 1px;
border: 1px solid var(--color-gray-200);
background-color: var(--color-gray-50);
border-radius: 0.375rem;
padding: 0.125rem;
}
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
padding: 0;
margin: 0;
outline: 0;
border: 0;
border-radius: 0.25rem;
background-color: transparent;
color: var(--color-gray-600);
user-select: none;
&:focus-visible {
background-color: transparent;
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-200);
}
&[data-pressed] {
color: var(--color-gray-900);
}
}
.Icon {
width: 1.25rem;
height: 1.25rem;
}
```
```tsx
/* index.tsx */
'use client';
import * as React from 'react';
import { Toggle } from '@base-ui/react/toggle';
import styles from './index.module.css';
export default function ExampleToggle() {
return (
<div className={styles.Panel}>
<Toggle
aria-label="Favorite"
className={styles.Button}
render={(props, state) => {
if (state.pressed) {
return (
<button type="button" {...props}>
<HeartFilledIcon className={styles.Icon} />
</button>
);
}
return (
<button type="button" {...props}>
<HeartOutlineIcon className={styles.Icon} />
</button>
);
}}
/>
</div>
);
}
function HeartFilledIcon(props: React.ComponentProps<'svg'>) {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentcolor" {...props}>
<path d="M7.99961 13.8667C7.88761 13.8667 7.77561 13.8315 7.68121 13.7611C7.43321 13.5766 1.59961 9.1963 1.59961 5.8667C1.59961 3.80856 3.27481 2.13336 5.33294 2.13336C6.59054 2.13336 7.49934 2.81176 7.99961 3.3131C8.49988 2.81176 9.40868 2.13336 10.6663 2.13336C12.7244 2.13336 14.3996 3.80803 14.3996 5.8667C14.3996 9.1963 8.56601 13.5766 8.31801 13.7616C8.22361 13.8315 8.11161 13.8667 7.99961 13.8667Z" />
</svg>
);
}
function HeartOutlineIcon(props: React.ComponentProps<'svg'>) {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentcolor" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.99961 4.8232L7.24456 4.06654C6.84123 3.66235 6.18866 3.20003 5.33294 3.20003C3.86391 3.20003 2.66628 4.39767 2.66628 5.8667C2.66628 6.4079 2.91276 7.1023 3.41967 7.91383C3.91548 8.70759 4.59649 9.51244 5.31278 10.2503C6.38267 11.3525 7.47318 12.2465 7.99983 12.6605C8.52734 12.2456 9.61718 11.352 10.6864 10.2504C11.4027 9.51248 12.0837 8.70762 12.5796 7.91384C13.0865 7.1023 13.3329 6.4079 13.3329 5.8667C13.3329 4.39723 12.1354 3.20003 10.6663 3.20003C9.81056 3.20003 9.15799 3.66235 8.75466 4.06654L7.99961 4.8232ZM7.98574 3.29926C7.48264 2.79938 6.57901 2.13336 5.33294 2.13336C3.27481 2.13336 1.59961 3.80856 1.59961 5.8667C1.59961 9.1963 7.43321 13.5766 7.68121 13.7611C7.77561 13.8315 7.88761 13.8667 7.99961 13.8667C8.11161 13.8667 8.22361 13.8315 8.31801 13.7616C8.56601 13.5766 14.3996 9.1963 14.3996 5.8667C14.3996 3.80803 12.7244 2.13336 10.6663 2.13336C9.42013 2.13336 8.51645 2.79947 8.01337 3.29936C8.00877 3.30393 8.00421 3.30849 7.99967 3.31303C7.99965 3.31305 7.99963 3.31307 7.99961 3.3131C7.99502 3.3085 7.9904 3.30389 7.98574 3.29926Z"
/>
</svg>
);
}
```
## Anatomy
Import the component and use it as a single part:
```jsx title="Anatomy"
import { Toggle } from '@base-ui/react/toggle';
<Toggle />;
```
## API reference
A two-state button that can be on or off.
Renders a `<button>` element.
**Toggle Props:**
| Prop | Type | Default | Description |
| :-------------- | :-------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| value | `string` | - | A unique string that identifies the toggle when used
inside a toggle group. |
| defaultPressed | `boolean` | `false` | Whether the toggle button is currently pressed.
This is the uncontrolled counterpart of `pressed`. |
| pressed | `boolean` | - | Whether the toggle button is currently pressed.
This is the controlled counterpart of `defaultPressed`. |
| onPressedChange | `((pressed: boolean, eventDetails: Toggle.ChangeEventDetails) => void)` | - | Callback fired when the pressed state is changed. |
| nativeButton | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it
via the `render` prop.
Set to `false` if the rendered element is not a button (e.g. `<div>`). |
| disabled | `boolean` | `false` | Whether the component should ignore user interaction. |
| className | `string \| ((state: Toggle.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Toggle.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Toggle.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Toggle Data Attributes:**
| Attribute | Type | Description |
| :----------- | :--- | :----------------------------------------- |
| data-pressed | - | Present when the toggle button is pressed. |
references/toolbar.md
---
title: Toolbar
subtitle: A container for grouping a set of buttons and controls.
description: A high-quality, unstyled React toolbar component that groups a set of buttons and controls.
---
# Toolbar
A high-quality, unstyled React toolbar component that groups a set of buttons and controls.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import * as React from "react";
import { Toolbar } from "@base-ui/react/toolbar";
import { ToggleGroup } from "@base-ui/react/toggle-group";
import { Toggle } from "@base-ui/react/toggle";
import { Select } from "@base-ui/react/select";
export default function ExampleToolbar() {
return (
<Toolbar.Root className="flex w-150 items-center gap-px rounded-md border border-gray-200 bg-gray-50 p-0.5">
<ToggleGroup className="flex gap-1" aria-label="Alignment">
<Toolbar.Button render={<Toggle />} aria-label="Align left" value="align-left" className="flex h-8 items-center justify-center rounded-sm px-[0.75rem] font-[inherit] text-sm font-medium text-gray-600 select-none hover:bg-gray-100 focus-visible:bg-none focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-200 data-[pressed]:bg-gray-100 data-[pressed]:text-gray-900">
Align Left
</Toolbar.Button>
<Toolbar.Button render={<Toggle />} aria-label="Align right" value="align-right" className="flex h-8 items-center justify-center rounded-sm px-[0.75rem] font-[inherit] text-sm font-medium text-gray-600 select-none hover:bg-gray-100 focus-visible:bg-none focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-200 data-[pressed]:bg-gray-100 data-[pressed]:text-gray-900">
Align Right
</Toolbar.Button>
</ToggleGroup>
<Toolbar.Separator className="m-1 h-4 w-px bg-gray-300" />
<Toolbar.Group className="flex gap-1" aria-label="Numerical format">
<Toolbar.Button className="flex size-8 items-center justify-center rounded-sm px-[0.75rem] font-[inherit] text-sm font-medium text-gray-600 select-none hover:bg-gray-100 focus-visible:bg-none focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-200 data-[pressed]:bg-gray-100 data-[pressed]:text-gray-900" aria-label="Format as currency">
$
</Toolbar.Button>
<Toolbar.Button className="flex size-8 items-center justify-center rounded-sm px-[0.75rem] font-[inherit] text-sm font-medium text-gray-600 select-none hover:bg-gray-100 focus-visible:bg-none focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-200 data-[pressed]:bg-gray-100 data-[pressed]:text-gray-900" aria-label="Format as percent">
%
</Toolbar.Button>
</Toolbar.Group>
<Toolbar.Separator className="m-1 h-4 w-px bg-gray-300" />
<Select.Root defaultValue="Helvetica">
<Toolbar.Button render={<Select.Trigger />} className="flex min-w-[8rem] h-8 text-sm font-medium items-center justify-between gap-3 rounded-md pr-3 pl-3.5 text-gray-600 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 data-[popup-open]:bg-gray-100 cursor-default">
<Select.Value />
<Select.Icon className="flex">
<ChevronUpDownIcon />
</Select.Icon>
</Toolbar.Button>
<Select.Portal>
<Select.Positioner className="outline-none select-none" sideOffset={8}>
<Select.Popup className="group max-h-[var(--available-height)] origin-[var(--transform-origin)] overflow-y-auto rounded-md bg-[canvas] py-1 text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[side=none]:data-[ending-style]:transition-none data-[starting-style]:scale-90 data-[starting-style]:opacity-0 data-[side=none]:data-[starting-style]:scale-100 data-[side=none]:data-[starting-style]:opacity-100 data-[side=none]:data-[starting-style]:transition-none dark:shadow-none dark:-outline-offset-1 dark:outline-gray-300">
<Select.Item value="Helvetica" className="grid min-w-[var(--anchor-width)] cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-1.5 pr-4 pl-2.5 leading-4 outline-none select-none group-data-[side=none]:min-w-[calc(var(--anchor-width)+1rem)] group-data-[side=none]:pr-12 group-data-[side=none]:leading-4 data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 pointer-coarse:py-2.5">
<Select.ItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Select.ItemIndicator>
<Select.ItemText className="col-start-2 text-sm">Helvetica</Select.ItemText>
</Select.Item>
<Select.Item value="Arial" className="grid min-w-[var(--anchor-width)] cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-1.5 pr-4 pl-2.5 leading-4 outline-none select-none group-data-[side=none]:min-w-[calc(var(--anchor-width)+1rem)] group-data-[side=none]:pr-12 group-data-[side=none]:leading-4 data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 pointer-coarse:py-2.5">
<Select.ItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Select.ItemIndicator>
<Select.ItemText className="col-start-2 text-sm">Arial</Select.ItemText>
</Select.Item>
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
<Toolbar.Separator className="m-1 h-4 w-px bg-gray-300" />
<Toolbar.Link className="mr-[0.875rem] ml-auto flex-none self-center text-sm text-gray-500 no-underline hover:text-blue-800 focus-visible:rounded-sm focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-800" href="#">
Edited 51m ago
</Toolbar.Link>
</Toolbar.Root>
);
}
function ChevronUpDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" stroke="currentcolor" strokeWidth="1.5" {...props}>
<path d="M0.5 4.5L4 1.5L7.5 4.5" />
<path d="M0.5 7.5L4 10.5L7.5 7.5" />
</svg>
);
}
function CheckIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="10" height="10" viewBox="0 0 10 10" {...props}>
<path d="M9.1603 1.12218C9.50684 1.34873 9.60427 1.81354 9.37792 2.16038L5.13603 8.66012C5.01614 8.8438 4.82192 8.96576 4.60451 8.99384C4.3871 9.02194 4.1683 8.95335 4.00574 8.80615L1.24664 6.30769C0.939709 6.02975 0.916013 5.55541 1.19372 5.24822C1.47142 4.94102 1.94536 4.91731 2.2523 5.19524L4.36085 7.10461L8.12299 1.33999C8.34934 0.993152 8.81376 0.895638 9.1603 1.12218Z" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Toolbar {
display: flex;
align-items: center;
gap: 1px;
border: 1px solid var(--color-gray-200);
background-color: var(--color-gray-50);
border-radius: 0.375rem;
padding: 0.125rem;
width: 37.5rem;
}
.Group {
display: flex;
gap: 0.25rem;
}
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
min-width: 2rem;
height: 2rem;
padding: 0;
margin: 0;
outline: 0;
border: 0;
border-radius: 0.25rem;
background-color: transparent;
color: var(--color-gray-600);
user-select: none;
font-family: inherit;
font-size: 0.875rem;
font-weight: 500;
&:focus-visible {
background-color: transparent;
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-200);
}
&[data-pressed] {
background-color: var(--color-gray-100);
color: var(--color-gray-900);
}
&[aria-pressed] {
padding: 0 0.75rem;
}
&[role="combobox"] {
min-width: 8rem;
justify-content: space-between;
padding: 0 0.75rem;
}
}
.Separator {
width: 1px;
height: 16px;
margin: 0.25rem;
background-color: var(--color-gray-300);
}
.Link {
color: var(--color-gray-500);
font-family: inherit;
font-size: 0.875rem;
text-decoration: none;
align-self: center;
flex: 0 0 auto;
margin-inline: auto 0.875rem;
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -2px;
border-radius: var(--radius-sm);
}
@media (hover: hover) {
&:hover {
color: var(--color-blue);
}
}
}
.Select {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
height: 2.5rem;
padding-left: 0.875rem;
padding-right: 0.75rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
font-family: inherit;
font-size: 1rem;
line-height: 1.5rem;
color: var(--color-gray-900);
cursor: default;
-webkit-user-select: none;
user-select: none;
min-width: 9rem;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.SelectIcon {
display: flex;
}
.Positioner {
outline: 0;
-webkit-user-select: none;
user-select: none;
z-index: 1;
}
.Popup {
box-sizing: border-box;
padding-block: 0.25rem;
border-radius: 0.375rem;
background-color: canvas;
color: var(--color-gray-900);
transform-origin: var(--transform-origin);
transition: transform 150ms, opacity 150ms;
overflow-y: auto;
max-height: var(--available-height);
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
&[data-side="none"] {
transition: none;
transform: none;
opacity: 1;
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow: 0 10px 15px -3px var(--color-gray-200), 0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.Item {
box-sizing: border-box;
outline: 0;
line-height: 1rem;
padding-block: 0.5rem;
padding-left: 0.625rem;
padding-right: 1rem;
min-width: var(--anchor-width);
display: grid;
gap: 0.5rem;
align-items: center;
grid-template-columns: 0.75rem 1fr;
cursor: default;
-webkit-user-select: none;
user-select: none;
scroll-margin-block: 1rem;
font-size: 0.875rem;
@media (pointer: coarse) {
padding-block: 0.625rem;
}
[data-side="none"] & {
padding-right: 3rem;
min-width: calc(var(--anchor-width) + 1rem);
}
&[data-highlighted] {
z-index: 0;
position: relative;
color: var(--color-gray-50);
}
&[data-highlighted]::before {
content: "";
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0.25rem;
border-radius: 0.25rem;
background-color: var(--color-gray-900);
}
}
.ItemIndicator {
grid-column-start: 1;
}
.ItemIndicatorIcon {
display: block;
width: 0.75rem;
height: 0.75rem;
}
.ItemText {
grid-column-start: 2;
}
```
```tsx
/* index.tsx */
import * as React from "react";
import { Toolbar } from "@base-ui/react/toolbar";
import { ToggleGroup } from "@base-ui/react/toggle-group";
import { Toggle } from "@base-ui/react/toggle";
import { Select } from "@base-ui/react/select";
import styles from "./index.module.css";
export default function ExampleToolbar() {
return (
<Toolbar.Root className={styles.Toolbar}>
<ToggleGroup className={styles.Group} aria-label="Alignment">
<Toolbar.Button render={<Toggle />} aria-label="Align left" value="align-left" className={styles.Button}>
Align Left
</Toolbar.Button>
<Toolbar.Button render={<Toggle />} aria-label="Align right" value="align-right" className={styles.Button}>
Align Right
</Toolbar.Button>
</ToggleGroup>
<Toolbar.Separator className={styles.Separator} />
<Toolbar.Group className={styles.Group} aria-label="Numerical format">
<Toolbar.Button className={styles.Button} aria-label="Format as currency">
$
</Toolbar.Button>
<Toolbar.Button className={styles.Button} aria-label="Format as percent">
%
</Toolbar.Button>
</Toolbar.Group>
<Toolbar.Separator className={styles.Separator} />
<Select.Root defaultValue="Helvetica">
<Toolbar.Button render={<Select.Trigger />} className={styles.Button}>
<Select.Value />
<Select.Icon className={styles.SelectIcon}>
<ChevronUpDownIcon />
</Select.Icon>
</Toolbar.Button>
<Select.Portal>
<Select.Positioner className={styles.Positioner} sideOffset={8}>
<Select.Popup className={styles.Popup}>
<Select.Item className={styles.Item} value="Helvetica">
<Select.ItemIndicator className={styles.ItemIndicator}>
<CheckIcon className={styles.ItemIndicatorIcon} />
</Select.ItemIndicator>
<Select.ItemText className={styles.ItemText}>Helvetica</Select.ItemText>
</Select.Item>
<Select.Item className={styles.Item} value="Arial">
<Select.ItemIndicator className={styles.ItemIndicator}>
<CheckIcon className={styles.ItemIndicatorIcon} />
</Select.ItemIndicator>
<Select.ItemText className={styles.ItemText}>Arial</Select.ItemText>
</Select.Item>
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
<Toolbar.Separator className={styles.Separator} />
<Toolbar.Link className={styles.Link} href="#">
Edited 51m ago
</Toolbar.Link>
</Toolbar.Root>
);
}
function ChevronUpDownIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" stroke="currentcolor" strokeWidth="1.5" {...props}>
<path d="M0.5 4.5L4 1.5L7.5 4.5" />
<path d="M0.5 7.5L4 10.5L7.5 7.5" />
</svg>
);
}
function CheckIcon(props: React.ComponentProps<"svg">) {
return (
<svg fill="currentcolor" width="10" height="10" viewBox="0 0 10 10" {...props}>
<path d="M9.1603 1.12218C9.50684 1.34873 9.60427 1.81354 9.37792 2.16038L5.13603 8.66012C5.01614 8.8438 4.82192 8.96576 4.60451 8.99384C4.3871 9.02194 4.1683 8.95335 4.00574 8.80615L1.24664 6.30769C0.939709 6.02975 0.916013 5.55541 1.19372 5.24822C1.47142 4.94102 1.94536 4.91731 2.2523 5.19524L4.36085 7.10461L8.12299 1.33999C8.34934 0.993152 8.81376 0.895638 9.1603 1.12218Z" />
</svg>
);
}
```
## Usage guidelines
To ensure that toolbars are accessible and helpful, follow these guidelines:
- **Use inputs sparingly**: Left and right arrow keys are used to both move the text insertion cursor in an input, and to navigate among controls in horizontal toolbars. When using an input in a horizontal toolbar, use only one and place it as the last element of the toolbar.
## Anatomy
Import the component and assemble its parts:
```jsx title="Anatomy"
import { Toolbar } from "@base-ui/react/toolbar";
<Toolbar.Root>
<Toolbar.Button />
<Toolbar.Link />
<Toolbar.Separator />
<Toolbar.Group>
<Toolbar.Button />
<Toolbar.Button />
</Toolbar.Group>
<Toolbar.Input />
</Toolbar.Root>;
```
## Examples
### Using with Menu
All Base UI popup components that provide a `Trigger` component can be integrated with a toolbar by passing the trigger to `<Toolbar.Button>` with the `render` prop:
```tsx {4,12} title="Using popups with toolbar"
return (
<Toolbar.Root>
<Menu.Root>
<Toolbar.Button render={<Menu.Trigger />} />
<Menu.Portal>
{/* prettier-ignore */}
{/* Compose the rest of the menu */}
</Menu.Portal>
</Menu.Root>
</Toolbar.Root>;
)
```
This applies to `<AlertDialog>`, `<Dialog>`, `<Menu>`, `<Popover>`, and `<Select>`.
### Using with Tooltip
Unlike other popups, the toolbar item should be passed to the `render` prop of `<Tooltip.Trigger>`:
```tsx {4} title="Using popups with toolbar"
return (
<Toolbar.Root>
<Tooltip.Root>
<Tooltip.Trigger render={<Toolbar.Button />} />
<Tooltip.Portal>
{/* prettier-ignore */}
{/* Compose the rest of the tooltip */}
</Tooltip.Portal>
</Tooltip.Root>
</Toolbar.Root>;
)
```
### Using with NumberField
To use a NumberField in the toolbar, pass `<NumberField.Input>` to `<Toolbar.Input>` using the `render` prop:
```tsx {6} title="Using NumberField with toolbar"
return (
<Toolbar.Root>
<NumberField.Root>
<NumberField.Group>
<NumberField.Decrement />
<Toolbar.Input render={<NumberField.Input />} />
<NumberField.Increment />
</NumberField.Group>
</NumberField.Root>
</Toolbar.Root>;
)
```
## API reference
### Root
A container for grouping a set of controls, such as buttons, toggle groups, or menus.
Renders a `<div>` element.
**Root Props:**
| Prop | Type | Default | Description |
| :---------- | :-------------------------------------------------------------------------------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| loopFocus | `boolean` | `true` | If `true`, using keyboard navigation will wrap focus to the other end of the toolbar once the end is reached. |
| disabled | `boolean` | - | - |
| orientation | `Toolbar.Root.Orientation` | `'horizontal'` | The orientation of the toolbar. |
| className | `string \| ((state: Toolbar.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Toolbar.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Toolbar.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Root Data Attributes:**
| Attribute | Type | Description |
| :--------------- | :--------------------------- | :---------------------------------------- |
| data-orientation | `'horizontal' \| 'vertical'` | Indicates the orientation of the toolbar. |
| data-disabled | - | Present when the toolbar is disabled. |
### Button
A button that can be used as-is or as a trigger for other components.
Renders a `<button>` element.
**Button Props:**
| Prop | Type | Default | Description |
| :-------------------- | :---------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| focusableWhenDisabled | `boolean` | `true` | When `true` the item remains focuseable when disabled. |
| nativeButton | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it
via the `render` prop.
Set to `false` if the rendered element is not a button (e.g. `<div>`). |
| disabled | `boolean` | `false` | When `true` the item is disabled. |
| className | `string \| ((state: Toolbar.Button.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Toolbar.Button.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Toolbar.Button.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Button Data Attributes:**
| Attribute | Type | Description |
| :--------------- | :--------------------------- | :------------------------------------------------------- |
| data-orientation | `'horizontal' \| 'vertical'` | Indicates the orientation of the toolbar. |
| data-disabled | - | Present when the button is disabled. |
| data-focusable | - | Present when the button remains focusable when disabled. |
### Link
A link component.
Renders an `<a>` element.
**Link Props:**
| Prop | Type | Default | Description |
| :-------- | :-------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Toolbar.Link.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Toolbar.Link.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Toolbar.Link.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Link Data Attributes:**
| Attribute | Type | Description |
| :--------------- | :--------------------------- | :---------------------------------------- |
| data-orientation | `'horizontal' \| 'vertical'` | Indicates the orientation of the toolbar. |
### Input
A native input element that integrates with Toolbar keyboard navigation.
Renders an `<input>` element.
**Input Props:**
| Prop | Type | Default | Description |
| :-------------------- | :--------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| defaultValue | `string \| number \| string[]` | - | - |
| focusableWhenDisabled | `boolean` | `true` | When `true` the item remains focuseable when disabled. |
| disabled | `boolean` | `false` | When `true` the item is disabled. |
| className | `string \| ((state: Toolbar.Input.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Toolbar.Input.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Toolbar.Input.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Input Data Attributes:**
| Attribute | Type | Description |
| :--------------- | :--------------------------- | :------------------------------------------------------ |
| data-orientation | `'horizontal' \| 'vertical'` | Indicates the orientation of the toolbar. |
| data-disabled | - | Present when the input is disabled. |
| data-focusable | - | Present when the input remains focusable when disabled. |
### Group
Groups several toolbar items or toggles.
Renders a `<div>` element.
**Group Props:**
| Prop | Type | Default | Description |
| :-------- | :-------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| disabled | `boolean` | `false` | When `true` all toolbar items in the group are disabled. |
| className | `string \| ((state: Toolbar.Root.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Toolbar.Root.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Toolbar.Root.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Group Data Attributes:**
| Attribute | Type | Description |
| :--------------- | :--------------------------- | :---------------------------------------- |
| data-orientation | `'horizontal' \| 'vertical'` | Indicates the orientation of the toolbar. |
| data-disabled | - | Present when the group is disabled. |
### Separator
A separator element accessible to screen readers.
Renders a `<div>` element.
**Separator Props:**
| Prop | Type | Default | Description |
| :---------- | :---------------------------------------------------------------------------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| orientation | `Orientation` | `'horizontal'` | The orientation of the separator. |
| className | `string \| ((state: SeparatorState) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: SeparatorState) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: SeparatorState) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Separator Data Attributes:**
| Attribute | Type | Description |
| :--------------- | :--------------------------- | :---------------------------------------- |
| data-orientation | `'horizontal' \| 'vertical'` | Indicates the orientation of the toolbar. |
references/tooltip.md
---
title: Tooltip
subtitle: A popup that appears when an element is hovered or focused, showing a hint for sighted users.
description: A high-quality, unstyled React tooltip component that appears when an element is hovered or focused, showing a hint for sighted users.
---
# Tooltip
<Meta name="description" content="A high-quality, unstyled React tooltip component that appears when an element is hovered or focused, showing a hint for sighted users." />
## v1.3.0 notes
- `Tooltip` adds `closeOnClick` for flows where clicking the trigger or target action should also dismiss the hint.
- Keep using tooltips as non-essential hints for sighted users; do not move required form or validation messaging into tooltip-only content.
## v1.9.0 notes
- Automatic unmounting is decoupled from `actionsRef`. Call `eventDetails.preventUnmountOnClose()` in `onOpenChange` to opt out, then end the closing phase with `actionsRef.current.unmount()`. See `popover.md`.
- A viewport transition no longer starts when a retained popup reopens.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
import * as React from "react";
import { Tooltip } from "@base-ui/react/tooltip";
export default function ExampleTooltip() {
return (
<Tooltip.Provider>
<div className="flex border border-gray-200 rounded-md bg-gray-50 p-0.5">
<Tooltip.Root>
<Tooltip.Trigger
className="
flex size-8 items-center justify-center
border-0 rounded-sm
bg-transparent
text-gray-900
select-none
data-popup-open:bg-gray-100
focus-visible:bg-none
focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800
hover:bg-gray-100
active:bg-gray-200
focus-visible:not-[&:hover]:bg-transparent"
>
<BoldIcon aria-label="Bold" className="size-4" />
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner sideOffset={10}>
<Tooltip.Popup
className="
flex flex-col
px-2 py-1
rounded-md
bg-[canvas]
text-sm
origin-(--transform-origin)
shadow-lg shadow-gray-200 outline-1 outline-gray-200
transition-[transform,scale,opacity]
data-ending-style:opacity-0 data-ending-style:scale-90
data-instant:transition-none
data-starting-style:opacity-0 data-starting-style:scale-90
dark:shadow-none dark:outline-gray-300 dark:-outline-offset-1"
>
<Tooltip.Arrow
className="
flex
data-[side=bottom]:-top-2 data-[side=bottom]:rotate-0
data-[side=left]:right-[-13px] data-[side=left]:rotate-90
data-[side=right]:left-[-13px] data-[side=right]:-rotate-90
data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
>
<ArrowSvg />
</Tooltip.Arrow>
Bold
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
</Tooltip.Root>
<Tooltip.Root>
<Tooltip.Trigger
className="
flex size-8 items-center justify-center
border-0 rounded-sm
bg-transparent
text-gray-900
select-none
data-popup-open:bg-gray-100
focus-visible:bg-none
focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800
hover:bg-gray-100
active:bg-gray-200
focus-visible:not-[&:hover]:bg-transparent"
>
<ItalicIcon aria-label="Italic" className="size-4" />
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner sideOffset={10}>
<Tooltip.Popup
className="
flex flex-col
px-2 py-1
rounded-md
bg-[canvas]
text-sm
origin-(--transform-origin)
shadow-lg shadow-gray-200 outline-1 outline-gray-200
transition-[transform,scale,opacity]
data-ending-style:opacity-0 data-ending-style:scale-90
data-instant:transition-none
data-starting-style:opacity-0 data-starting-style:scale-90
dark:shadow-none dark:outline-gray-300 dark:-outline-offset-1"
>
<Tooltip.Arrow
className="
flex
data-[side=bottom]:-top-2 data-[side=bottom]:rotate-0
data-[side=left]:right-[-13px] data-[side=left]:rotate-90
data-[side=right]:left-[-13px] data-[side=right]:-rotate-90
data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
>
<ArrowSvg />
</Tooltip.Arrow>
Italic
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
</Tooltip.Root>
<Tooltip.Root>
<Tooltip.Trigger
className="
flex size-8 items-center justify-center
border-0 rounded-sm
bg-transparent
text-gray-900
select-none
data-popup-open:bg-gray-100
focus-visible:bg-none
focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800
hover:bg-gray-100
active:bg-gray-200
focus-visible:not-[&:hover]:bg-transparent"
>
<UnderlineIcon aria-label="Underline" className="size-4" />
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner sideOffset={10}>
<Tooltip.Popup
className="
flex flex-col
px-2 py-1
rounded-md
bg-[canvas]
text-sm
origin-(--transform-origin)
shadow-lg shadow-gray-200 outline-1 outline-gray-200
transition-[transform,scale,opacity]
data-ending-style:opacity-0 data-ending-style:scale-90
data-instant:transition-none
data-starting-style:opacity-0 data-starting-style:scale-90
dark:shadow-none dark:outline-gray-300 dark:-outline-offset-1"
>
<Tooltip.Arrow
className="
flex
data-[side=bottom]:-top-2 data-[side=bottom]:rotate-0
data-[side=left]:right-[-13px] data-[side=left]:rotate-90
data-[side=right]:left-[-13px] data-[side=right]:-rotate-90
data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
>
<ArrowSvg />
</Tooltip.Arrow>
Underline
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
</Tooltip.Root>
</div>
</Tooltip.Provider>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className="fill-[canvas]" />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className="fill-gray-200 dark:fill-none" />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className="dark:fill-gray-300" />
</svg>
);
}
function BoldIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentcolor" {...props}>
<path d="M3.73353 2.13333C3.4386 2.13333 3.2002 2.37226 3.2002 2.66666C3.2002 2.96106 3.4386 3.2 3.73353 3.2H4.26686V12.8H3.73353C3.4386 12.8 3.2002 13.0389 3.2002 13.3333C3.2002 13.6277 3.4386 13.8667 3.73353 13.8667H9.86686C11.7783 13.8667 13.3335 12.3115 13.3335 10.4C13.3335 8.9968 12.4945 7.78881 11.2929 7.24375C11.8897 6.70615 12.2669 5.93066 12.2669 5.06666C12.2669 3.44906 10.9506 2.13333 9.33353 2.13333H3.73353ZM6.93353 3.2H8.26686C9.29619 3.2 10.1335 4.03733 10.1335 5.06666C10.1335 6.096 9.29619 6.93333 8.26686 6.93333H6.93353V3.2ZM6.93353 8H7.73353H8.26686C9.59006 8 10.6669 9.0768 10.6669 10.4C10.6669 11.7232 9.59006 12.8 8.26686 12.8H6.93353V8Z" />
</svg>
);
}
function ItalicIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentcolor" {...props}>
<path d="M8.52599 2.12186C8.48583 2.12267 8.44578 2.1265 8.4062 2.13332H6.93328C6.86261 2.13232 6.79244 2.14538 6.72686 2.17173C6.66127 2.19808 6.60158 2.23721 6.55125 2.28683C6.50092 2.33646 6.46096 2.39559 6.43368 2.46079C6.4064 2.526 6.39235 2.59597 6.39235 2.66665C6.39235 2.73733 6.4064 2.80731 6.43368 2.87251C6.46096 2.93772 6.50092 2.99685 6.55125 3.04647C6.60158 3.0961 6.66127 3.13522 6.72686 3.16157C6.79244 3.18793 6.86261 3.20099 6.93328 3.19999H7.70099L6.69057 12.8H5.86661C5.79594 12.799 5.72577 12.812 5.66019 12.8384C5.59461 12.8648 5.53492 12.9039 5.48459 12.9535C5.43425 13.0031 5.39429 13.0623 5.36701 13.1275C5.33973 13.1927 5.32568 13.2626 5.32568 13.3333C5.32568 13.404 5.33973 13.474 5.36701 13.5392C5.39429 13.6044 5.43425 13.6635 5.48459 13.7131C5.53492 13.7628 5.59461 13.8019 5.66019 13.8282C5.72577 13.8546 5.79594 13.8677 5.86661 13.8667H9.06661C9.13729 13.8677 9.20745 13.8546 9.27304 13.8282C9.33862 13.8019 9.39831 13.7628 9.44864 13.7131C9.49897 13.6635 9.53894 13.6044 9.56622 13.5392C9.5935 13.474 9.60754 13.404 9.60754 13.3333C9.60754 13.2626 9.5935 13.1927 9.56622 13.1275C9.53894 13.0623 9.49897 13.0031 9.44864 12.9535C9.39831 12.9039 9.33862 12.8648 9.27304 12.8384C9.20745 12.812 9.13729 12.799 9.06661 12.8H8.2989L9.30932 3.19999H10.1333C10.204 3.20099 10.2741 3.18793 10.3397 3.16157C10.4053 3.13522 10.465 3.0961 10.5153 3.04647C10.5656 2.99685 10.6056 2.93772 10.6329 2.87251C10.6602 2.80731 10.6742 2.73733 10.6742 2.66665C10.6742 2.59597 10.6602 2.526 10.6329 2.46079C10.6056 2.39559 10.5656 2.33646 10.5153 2.28683C10.465 2.23721 10.4053 2.19808 10.3397 2.17173C10.2741 2.14538 10.204 2.13232 10.1333 2.13332H8.66349C8.61807 2.12555 8.57207 2.12171 8.52599 2.12186Z" />
</svg>
);
}
function UnderlineIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentcolor" {...props}>
<path d="M3.73331 2.13332C3.66264 2.13232 3.59247 2.14538 3.52689 2.17173C3.46131 2.19809 3.40161 2.23721 3.35128 2.28684C3.30095 2.33646 3.26099 2.39559 3.23371 2.4608C3.20643 2.526 3.19238 2.59598 3.19238 2.66666C3.19238 2.73734 3.20643 2.80731 3.23371 2.87252C3.26099 2.93772 3.30095 2.99685 3.35128 3.04648C3.40161 3.0961 3.46131 3.13523 3.52689 3.16158C3.59247 3.18793 3.66264 3.20099 3.73331 3.19999V7.99999C3.73331 10.224 5.55144 12.2667 7.99998 12.2667C10.4485 12.2667 12.2666 10.224 12.2666 7.99999V3.19999C12.3373 3.20099 12.4075 3.18793 12.4731 3.16158C12.5386 3.13523 12.5983 3.0961 12.6487 3.04648C12.699 2.99685 12.739 2.93772 12.7662 2.87252C12.7935 2.80731 12.8076 2.73734 12.8076 2.66666C12.8076 2.59598 12.7935 2.526 12.7662 2.4608C12.739 2.39559 12.699 2.33646 12.6487 2.28684C12.5983 2.23721 12.5386 2.19809 12.4731 2.17173C12.4075 2.14538 12.3373 2.13232 12.2666 2.13332H10.1333C10.0626 2.13232 9.99247 2.14538 9.92689 2.17173C9.8613 2.19809 9.80161 2.23721 9.75128 2.28684C9.70095 2.33646 9.66099 2.39559 9.63371 2.4608C9.60643 2.526 9.59238 2.59598 9.59238 2.66666C9.59238 2.73734 9.60643 2.80731 9.63371 2.87252C9.66099 2.93772 9.70095 2.99685 9.75128 3.04648C9.80161 3.0961 9.8613 3.13523 9.92689 3.16158C9.99247 3.18793 10.0626 3.20099 10.1333 3.19999V8.97187C10.1333 10.0855 9.32179 11.0818 8.21352 11.1896C6.94152 11.3138 5.86665 10.3136 5.86665 9.06666V3.19999C5.93732 3.20099 6.00748 3.18793 6.07307 3.16158C6.13865 3.13523 6.19834 3.0961 6.24867 3.04648C6.299 2.99685 6.33897 2.93772 6.36625 2.87252C6.39353 2.80731 6.40757 2.73734 6.40757 2.66666C6.40757 2.59598 6.39353 2.526 6.36625 2.4608C6.33897 2.39559 6.299 2.33646 6.24867 2.28684C6.19834 2.23721 6.13865 2.19809 6.07307 2.17173C6.00748 2.14538 5.93732 2.13232 5.86665 2.13332H3.73331ZM3.73331 13.3333C3.66264 13.3323 3.59247 13.3454 3.52689 13.3717C3.46131 13.3981 3.40161 13.4372 3.35128 13.4868C3.30095 13.5365 3.26099 13.5956 3.23371 13.6608C3.20643 13.726 3.19238 13.796 3.19238 13.8667C3.19238 13.9373 3.20643 14.0073 3.23371 14.0725C3.26099 14.1377 3.30095 14.1969 3.35128 14.2465C3.40161 14.2961 3.46131 14.3352 3.52689 14.3616C3.59247 14.3879 3.66264 14.401 3.73331 14.4H12.2666C12.3373 14.401 12.4075 14.3879 12.4731 14.3616C12.5386 14.3352 12.5983 14.2961 12.6487 14.2465C12.699 14.1969 12.739 14.1377 12.7662 14.0725C12.7935 14.0073 12.8076 13.9373 12.8076 13.8667C12.8076 13.796 12.7935 13.726 12.7662 13.6608C12.739 13.5956 12.699 13.5365 12.6487 13.4868C12.5983 13.4372 12.5386 13.3981 12.4731 13.3717C12.4075 13.3454 12.3373 13.3323 12.2666 13.3333H3.73331Z" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Panel {
display: flex;
border: 1px solid var(--color-gray-200);
background-color: var(--color-gray-50);
border-radius: 0.375rem;
padding: 0.125rem;
}
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
padding: 0;
margin: 0;
outline: 0;
border: 0;
border-radius: 0.25rem;
background-color: transparent;
color: var(--color-gray-900);
user-select: none;
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
background-color: transparent;
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-200);
}
}
.Icon {
width: 1rem;
height: 1rem;
}
.Popup {
box-sizing: border-box;
font-size: 0.875rem;
line-height: 1.25rem;
display: flex;
flex-direction: column;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
background-color: canvas;
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
&[data-instant] {
transition: none;
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
```
```tsx
/* index.tsx */
import * as React from "react";
import { Tooltip } from "@base-ui/react/tooltip";
import styles from "./index.module.css";
export default function ExampleTooltip() {
return (
<Tooltip.Provider>
<div className={styles.Panel}>
<Tooltip.Root>
<Tooltip.Trigger aria-label="Bold" className={styles.Button}>
<BoldIcon className={styles.Icon} />
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner sideOffset={10}>
<Tooltip.Popup className={styles.Popup}>
<Tooltip.Arrow className={styles.Arrow}>
<ArrowSvg />
</Tooltip.Arrow>
Bold
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
</Tooltip.Root>
<Tooltip.Root>
<Tooltip.Trigger aria-label="Italic" className={styles.Button}>
<ItalicIcon className={styles.Icon} />
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner sideOffset={10}>
<Tooltip.Popup className={styles.Popup}>
<Tooltip.Arrow className={styles.Arrow}>
<ArrowSvg />
</Tooltip.Arrow>
Italic
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
</Tooltip.Root>
<Tooltip.Root>
<Tooltip.Trigger aria-label="Underline" className={styles.Button}>
<UnderlineIcon className={styles.Icon} />
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner sideOffset={10}>
<Tooltip.Popup className={styles.Popup}>
<Tooltip.Arrow className={styles.Arrow}>
<ArrowSvg />
</Tooltip.Arrow>
Underline
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
</Tooltip.Root>
</div>
</Tooltip.Provider>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className={styles.ArrowFill} />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className={styles.ArrowOuterStroke} />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className={styles.ArrowInnerStroke} />
</svg>
);
}
function BoldIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentcolor" {...props}>
<path d="M3.73353 2.13333C3.4386 2.13333 3.2002 2.37226 3.2002 2.66666C3.2002 2.96106 3.4386 3.2 3.73353 3.2H4.26686V12.8H3.73353C3.4386 12.8 3.2002 13.0389 3.2002 13.3333C3.2002 13.6277 3.4386 13.8667 3.73353 13.8667H9.86686C11.7783 13.8667 13.3335 12.3115 13.3335 10.4C13.3335 8.9968 12.4945 7.78881 11.2929 7.24375C11.8897 6.70615 12.2669 5.93066 12.2669 5.06666C12.2669 3.44906 10.9506 2.13333 9.33353 2.13333H3.73353ZM6.93353 3.2H8.26686C9.29619 3.2 10.1335 4.03733 10.1335 5.06666C10.1335 6.096 9.29619 6.93333 8.26686 6.93333H6.93353V3.2ZM6.93353 8H7.73353H8.26686C9.59006 8 10.6669 9.0768 10.6669 10.4C10.6669 11.7232 9.59006 12.8 8.26686 12.8H6.93353V8Z" />
</svg>
);
}
function ItalicIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentcolor" {...props}>
<path d="M8.52599 2.12186C8.48583 2.12267 8.44578 2.1265 8.4062 2.13332H6.93328C6.86261 2.13232 6.79244 2.14538 6.72686 2.17173C6.66127 2.19808 6.60158 2.23721 6.55125 2.28683C6.50092 2.33646 6.46096 2.39559 6.43368 2.46079C6.4064 2.526 6.39235 2.59597 6.39235 2.66665C6.39235 2.73733 6.4064 2.80731 6.43368 2.87251C6.46096 2.93772 6.50092 2.99685 6.55125 3.04647C6.60158 3.0961 6.66127 3.13522 6.72686 3.16157C6.79244 3.18793 6.86261 3.20099 6.93328 3.19999H7.70099L6.69057 12.8H5.86661C5.79594 12.799 5.72577 12.812 5.66019 12.8384C5.59461 12.8648 5.53492 12.9039 5.48459 12.9535C5.43425 13.0031 5.39429 13.0623 5.36701 13.1275C5.33973 13.1927 5.32568 13.2626 5.32568 13.3333C5.32568 13.404 5.33973 13.474 5.36701 13.5392C5.39429 13.6044 5.43425 13.6635 5.48459 13.7131C5.53492 13.7628 5.59461 13.8019 5.66019 13.8282C5.72577 13.8546 5.79594 13.8677 5.86661 13.8667H9.06661C9.13729 13.8677 9.20745 13.8546 9.27304 13.8282C9.33862 13.8019 9.39831 13.7628 9.44864 13.7131C9.49897 13.6635 9.53894 13.6044 9.56622 13.5392C9.5935 13.474 9.60754 13.404 9.60754 13.3333C9.60754 13.2626 9.5935 13.1927 9.56622 13.1275C9.53894 13.0623 9.49897 13.0031 9.44864 12.9535C9.39831 12.9039 9.33862 12.8648 9.27304 12.8384C9.20745 12.812 9.13729 12.799 9.06661 12.8H8.2989L9.30932 3.19999H10.1333C10.204 3.20099 10.2741 3.18793 10.3397 3.16157C10.4053 3.13522 10.465 3.0961 10.5153 3.04647C10.5656 2.99685 10.6056 2.93772 10.6329 2.87251C10.6602 2.80731 10.6742 2.73733 10.6742 2.66665C10.6742 2.59597 10.6602 2.526 10.6329 2.46079C10.6056 2.39559 10.5656 2.33646 10.5153 2.28683C10.465 2.23721 10.4053 2.19808 10.3397 2.17173C10.2741 2.14538 10.204 2.13232 10.1333 2.13332H8.66349C8.61807 2.12555 8.57207 2.12171 8.52599 2.12186Z" />
</svg>
);
}
function UnderlineIcon(props: React.ComponentProps<"svg">) {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentcolor" {...props}>
<path d="M3.73331 2.13332C3.66264 2.13232 3.59247 2.14538 3.52689 2.17173C3.46131 2.19809 3.40161 2.23721 3.35128 2.28684C3.30095 2.33646 3.26099 2.39559 3.23371 2.4608C3.20643 2.526 3.19238 2.59598 3.19238 2.66666C3.19238 2.73734 3.20643 2.80731 3.23371 2.87252C3.26099 2.93772 3.30095 2.99685 3.35128 3.04648C3.40161 3.0961 3.46131 3.13523 3.52689 3.16158C3.59247 3.18793 3.66264 3.20099 3.73331 3.19999V7.99999C3.73331 10.224 5.55144 12.2667 7.99998 12.2667C10.4485 12.2667 12.2666 10.224 12.2666 7.99999V3.19999C12.3373 3.20099 12.4075 3.18793 12.4731 3.16158C12.5386 3.13523 12.5983 3.0961 12.6487 3.04648C12.699 2.99685 12.739 2.93772 12.7662 2.87252C12.7935 2.80731 12.8076 2.73734 12.8076 2.66666C12.8076 2.59598 12.7935 2.526 12.7662 2.4608C12.739 2.39559 12.699 2.33646 12.6487 2.28684C12.5983 2.23721 12.5386 2.19809 12.4731 2.17173C12.4075 2.14538 12.3373 2.13232 12.2666 2.13332H10.1333C10.0626 2.13232 9.99247 2.14538 9.92689 2.17173C9.8613 2.19809 9.80161 2.23721 9.75128 2.28684C9.70095 2.33646 9.66099 2.39559 9.63371 2.4608C9.60643 2.526 9.59238 2.59598 9.59238 2.66666C9.59238 2.73734 9.60643 2.80731 9.63371 2.87252C9.66099 2.93772 9.70095 2.99685 9.75128 3.04648C9.80161 3.0961 9.8613 3.13523 9.92689 3.16158C9.99247 3.18793 10.0626 3.20099 10.1333 3.19999V8.97187C10.1333 10.0855 9.32179 11.0818 8.21352 11.1896C6.94152 11.3138 5.86665 10.3136 5.86665 9.06666V3.19999C5.93732 3.20099 6.00748 3.18793 6.07307 3.16158C6.13865 3.13523 6.19834 3.0961 6.24867 3.04648C6.299 2.99685 6.33897 2.93772 6.36625 2.87252C6.39353 2.80731 6.40757 2.73734 6.40757 2.66666C6.40757 2.59598 6.39353 2.526 6.36625 2.4608C6.33897 2.39559 6.299 2.33646 6.24867 2.28684C6.19834 2.23721 6.13865 2.19809 6.07307 2.17173C6.00748 2.14538 5.93732 2.13232 5.86665 2.13332H3.73331ZM3.73331 13.3333C3.66264 13.3323 3.59247 13.3454 3.52689 13.3717C3.46131 13.3981 3.40161 13.4372 3.35128 13.4868C3.30095 13.5365 3.26099 13.5956 3.23371 13.6608C3.20643 13.726 3.19238 13.796 3.19238 13.8667C3.19238 13.9373 3.20643 14.0073 3.23371 14.0725C3.26099 14.1377 3.30095 14.1969 3.35128 14.2465C3.40161 14.2961 3.46131 14.3352 3.52689 14.3616C3.59247 14.3879 3.66264 14.401 3.73331 14.4H12.2666C12.3373 14.401 12.4075 14.3879 12.4731 14.3616C12.5386 14.3352 12.5983 14.2961 12.6487 14.2465C12.699 14.1969 12.739 14.1377 12.7662 14.0725C12.7935 14.0073 12.8076 13.9373 12.8076 13.8667C12.8076 13.796 12.7935 13.726 12.7662 13.6608C12.739 13.5956 12.699 13.5365 12.6487 13.4868C12.5983 13.4372 12.5386 13.3981 12.4731 13.3717C12.4075 13.3454 12.3373 13.3323 12.2666 13.3333H3.73331Z" />
</svg>
);
}
```
## Usage guidelines
To ensure that tooltips are accessible and helpful, follow these guidelines:
- **Provide an accessible name for the trigger**: The tooltip's trigger must have a meaningful label. This can be its visible text or an `aria-label`/`aria-labelledby` attribute. The label should closely match the tooltip's content to ensure consistency for screen reader users.
- **Avoid tooltips for critical information**: Tooltips work well for enhancing UI clarity (like labeling icon buttons) but should not be the sole means of conveying important information. Since tooltips do not appear on touch devices, consider using a [Popover](/react/components/popover.md) for essential content.
- **Avoid tooltips for "infotips"**: If your tooltip is attached to an "info icon" button whose only purpose is to show the tooltip, opt for [Popover](/react/components/popover.md) and add the `openOnHover` prop instead. Tooltips should describe an element that performs an action separate from opening the tooltip itself.
- **Avoid for transient information**: Use the Toast component's [anchoring ability](/react/components/toast.md) instead of Tooltip for transient notifications, like a "Copied" popup that appears above a copy-to-clipboard button. This way, the message is announced to screen readers when it appears. Toasts can also contain more complex content, such as links or actions.
## Anatomy
Import the component and assemble its parts:
```jsx title="Anatomy"
import { Tooltip } from "@base-ui/react/tooltip";
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger />
<Tooltip.Portal>
<Tooltip.Positioner>
<Tooltip.Popup>
<Tooltip.Arrow />
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>;
```
## Examples
### Detached triggers
A tooltip can be controlled by a trigger located either inside or outside the `<Tooltip.Root>` component.
For simple, one-off interactions, place the `<Tooltip.Trigger>` inside `<Tooltip.Root>`, as shown in the example at the top of this page.
However, if defining the tooltip's content next to its trigger is not practical, you can use a detached trigger.
This involves placing the `<Tooltip.Trigger>` outside of `<Tooltip.Root>` and linking them with a `handle` created by the `Tooltip.createHandle()` function.
```jsx title="Detached triggers" {3,5} "handle={demoTooltip}"
const demoTooltip = Tooltip.createHandle();
<Tooltip.Trigger handle={demoTooltip}>Button</Tooltip.Trigger>
<Tooltip.Root handle={demoTooltip}>
...
</Tooltip.Root>
```
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Tooltip } from "@base-ui/react/tooltip";
import { ArrowSvg, InfoIcon } from "./icons-tw";
const demoTooltip = Tooltip.createHandle();
export default function TooltipDetachedTriggersSimpleDemo() {
return (
<Tooltip.Provider>
<Tooltip.Trigger
className="
flex size-10 items-center justify-center
border border-gray-200 rounded-md
bg-gray-50
text-gray-900
select-none
data-popup-open:bg-gray-100
focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800
hover:bg-gray-100
active:bg-gray-100"
handle={demoTooltip}
>
<InfoIcon aria-label="Information" />
</Tooltip.Trigger>
<Tooltip.Root handle={demoTooltip}>
<Tooltip.Portal>
<Tooltip.Positioner sideOffset={10}>
<Tooltip.Popup
className="
px-2 py-1
rounded-md
bg-[canvas]
text-sm
origin-(--transform-origin)
shadow-lg shadow-gray-200 outline-1 outline-gray-200
transition-[transform,scale,opacity]
data-ending-style:opacity-0 data-ending-style:scale-90
data-instant:transition-none
data-starting-style:opacity-0 data-starting-style:scale-90
dark:shadow-none dark:outline-gray-300 dark:-outline-offset-1"
>
<Tooltip.Arrow
className="
flex
data-[side=bottom]:-top-2 data-[side=bottom]:rotate-0
data-[side=left]:right-[-13px] data-[side=left]:rotate-90
data-[side=right]:left-[-13px] data-[side=right]:-rotate-90
data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
>
<ArrowSvg />
</Tooltip.Arrow>
This is a detached tooltip
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}
```
```tsx
/* icons-tw.tsx */
import * as React from "react";
export function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className="fill-[canvas]" />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className="fill-gray-200 dark:fill-none" />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className="dark:fill-gray-300" />
</svg>
);
}
export function InfoIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" {...props}>
<circle cx="12" cy="12" r="10" />
<path d="M12 16v-4" />
<path d="M12 8h.01" />
</svg>
);
}
export function HelpIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" {...props}>
<circle cx="12" cy="12" r="10" />
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
<path d="M12 17h.01" />
</svg>
);
}
export function AlertIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
<path d="M12 9v4" />
<path d="M12 17h.01" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Tooltip } from "@base-ui/react/tooltip";
import styles from "./demos.module.css";
const demoTooltip = Tooltip.createHandle();
export default function TooltipDetachedTriggersSimpleDemo() {
return (
<Tooltip.Provider>
<Tooltip.Trigger className={styles.IconButton} handle={demoTooltip}>
<InfoIcon aria-label="Information" className={styles.Icon} />
</Tooltip.Trigger>
<Tooltip.Root handle={demoTooltip}>
<Tooltip.Portal>
<Tooltip.Positioner sideOffset={10}>
<Tooltip.Popup className={styles.Popup}>
<Tooltip.Arrow className={styles.Arrow}>
<ArrowSvg />
</Tooltip.Arrow>
This is a detached tooltip
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className={styles.ArrowFill} />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className={styles.ArrowOuterStroke} />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className={styles.ArrowInnerStroke} />
</svg>
);
}
function InfoIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" {...props}>
<circle cx="12" cy="12" r="10" />
<path d="M12 16v-4" />
<path d="M12 8h.01" />
</svg>
);
}
```
```css
/* demos.module.css */
.Container {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: center;
}
.ButtonGroup {
display: flex;
}
.IconButton {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
padding: 0;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
background-color: var(--color-gray-50);
color: var(--color-gray-900);
user-select: none;
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:only-child {
border-radius: 0.375rem;
}
&:first-child:not(:only-child) {
border-radius: 0.375rem 0 0 0.375rem;
}
&:last-child:not(:only-child) {
border-radius: 0 0.375rem 0.375rem 0;
}
&:not(:first-child) {
border-left: none;
}
}
.Icon {
width: 1.25rem;
height: 1.25rem;
}
.Positioner {
height: var(--positioner-height);
width: var(--positioner-width);
max-width: var(--available-width);
}
.Popup {
box-sizing: border-box;
font-size: 0.875rem;
line-height: 1.25rem;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
background-color: canvas;
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
&[data-instant] {
transition: none;
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
```
### Multiple triggers
A single tooltip can be opened by multiple trigger elements.
You can achieve this by using the same `handle` for several detached triggers, or by placing multiple `<Tooltip.Trigger>` components inside a single `<Tooltip.Root>`.
```jsx title="Multiple triggers within the Root part"
<Tooltip.Root>
<Tooltip.Trigger>Trigger 1</Tooltip.Trigger>
<Tooltip.Trigger>Trigger 2</Tooltip.Trigger>
...
</Tooltip.Root>
```
```jsx title="Multiple detached triggers"
const demoTooltip = Tooltip.createHandle();
<Tooltip.Trigger handle={demoTooltip}>
Trigger 1
</Tooltip.Trigger>
<Tooltip.Trigger handle={demoTooltip}>
Trigger 2
</Tooltip.Trigger>
<Tooltip.Root handle={demoTooltip}>
...
</Tooltip.Root>
```
The tooltip can render different content depending on which trigger opened it.
This is achieved by passing a `payload` to the `<Tooltip.Trigger>` and using the function-as-a-child pattern in `<Tooltip.Root>`.
The payload can be strongly typed by providing a type argument to the `createHandle()` function:
```jsx title="Detached triggers with payload" {1,3,7} "payload"
const demoTooltip = Tooltip.createHandle<{ text: string }>();
<Tooltip.Trigger handle={demoTooltip} payload={{ text: 'Trigger 1' }}>
Trigger 1
</Tooltip.Trigger>
<Tooltip.Trigger handle={demoTooltip} payload={{ text: 'Trigger 2' }}>
Trigger 2
</Tooltip.Trigger>
<Tooltip.Root handle={demoTooltip}>
{({ payload }) => (
<Tooltip.Portal>
<Tooltip.Positioner sideOffset={8}>
<Tooltip.Popup className={styles.Popup}>
<Tooltip.Arrow className={styles.Arrow}>
<ArrowSvg />
</Tooltip.Arrow>
{payload !== undefined && (
<span>
Tooltip opened by {payload.text}
</span>
)}
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
)}
</Tooltip.Root>
```
### Controlled mode with multiple triggers
You can control the tooltip's open state externally using the `open` and `onOpenChange` props on `<Tooltip.Root>`.
This allows you to manage the tooltip's visibility based on your application's state.
When using multiple triggers, you have to manage which trigger is active with the `triggerId` prop on `<Tooltip.Root>` and the `id` prop on each `<Tooltip.Trigger>`.
Note that there is no separate `onTriggerIdChange` prop.
Instead, the `onOpenChange` callback receives an additional argument, `eventDetails`, which contains the trigger element that initiated the state change.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Tooltip } from "@base-ui/react/tooltip";
import { ArrowSvg, InfoIcon } from "./icons-tw";
const demoTooltip = Tooltip.createHandle();
export default function TooltipDetachedTriggersControlledDemo() {
const [open, setOpen] = React.useState(false);
const [triggerId, setTriggerId] = React.useState<string | null>(null);
const handleOpenChange = (isOpen: boolean, eventDetails: Tooltip.Root.ChangeEventDetails) => {
setOpen(isOpen);
setTriggerId(eventDetails.trigger?.id ?? null);
};
return (
<Tooltip.Provider>
<div className="flex gap-2 flex-wrap justify-center">
<div className="flex">
<Tooltip.Trigger
className="
flex size-10 items-center justify-center
border border-gray-200 rounded-l-md
bg-gray-50
text-gray-900
select-none
data-popup-open:bg-gray-100
focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800
hover:bg-gray-100
active:bg-gray-100"
handle={demoTooltip}
id="trigger-1"
>
<InfoIcon aria-label="Information 1" />
</Tooltip.Trigger>
<Tooltip.Trigger
className="
flex size-10 items-center justify-center
border-y border-r border-gray-200
bg-gray-50
text-gray-900
select-none
data-popup-open:bg-gray-100
focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800
hover:bg-gray-100
active:bg-gray-100"
handle={demoTooltip}
id="trigger-2"
>
<InfoIcon aria-label="Information 2" />
</Tooltip.Trigger>
<Tooltip.Trigger
className="
flex size-10 items-center justify-center
border-y border-r border-gray-200 rounded-r-md
bg-gray-50
text-gray-900
select-none
data-popup-open:bg-gray-100
focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800
hover:bg-gray-100
active:bg-gray-100"
handle={demoTooltip}
id="trigger-3"
>
<InfoIcon aria-label="Information 3" />
</Tooltip.Trigger>
</div>
<button
type="button"
className="
flex h-10 items-center justify-center
border border-gray-200 rounded-md
bg-gray-50
px-3.5
text-base font-medium text-gray-900
select-none
focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800
hover:bg-gray-100
active:bg-gray-100"
onClick={() => {
setTriggerId("trigger-2");
setOpen(true);
}}
>
Open programmatically
</button>
</div>
<Tooltip.Root handle={demoTooltip} open={open} onOpenChange={handleOpenChange} triggerId={triggerId}>
<Tooltip.Portal>
<Tooltip.Positioner
className="
h-(--positioner-height)
w-(--positioner-width)
max-w-(--available-width)
"
sideOffset={10}
>
<Tooltip.Popup
className="
px-2 py-1
rounded-md
bg-[canvas]
text-sm
origin-(--transform-origin)
shadow-lg shadow-gray-200 outline-1 outline-gray-200
transition-[transform,scale,opacity]
data-ending-style:opacity-0 data-ending-style:scale-90
data-instant:transition-none
data-starting-style:opacity-0 data-starting-style:scale-90
dark:shadow-none dark:outline-gray-300 dark:-outline-offset-1"
>
<Tooltip.Arrow
className="
flex
data-[side=bottom]:-top-2 data-[side=bottom]:rotate-0
data-[side=left]:right-[-13px] data-[side=left]:rotate-90
data-[side=right]:left-[-13px] data-[side=right]:-rotate-90
data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
>
<ArrowSvg />
</Tooltip.Arrow>
Controlled tooltip
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}
```
```tsx
/* icons-tw.tsx */
import * as React from "react";
export function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className="fill-[canvas]" />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className="fill-gray-200 dark:fill-none" />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className="dark:fill-gray-300" />
</svg>
);
}
export function InfoIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" {...props}>
<circle cx="12" cy="12" r="10" />
<path d="M12 16v-4" />
<path d="M12 8h.01" />
</svg>
);
}
export function HelpIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" {...props}>
<circle cx="12" cy="12" r="10" />
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
<path d="M12 17h.01" />
</svg>
);
}
export function AlertIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
<path d="M12 9v4" />
<path d="M12 17h.01" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Tooltip } from "@base-ui/react/tooltip";
import styles from "./demos.module.css";
const demoTooltip = Tooltip.createHandle();
export default function TooltipDetachedTriggersControlledDemo() {
const [open, setOpen] = React.useState(false);
const [triggerId, setTriggerId] = React.useState<string | null>(null);
const handleOpenChange = (isOpen: boolean, eventDetails: Tooltip.Root.ChangeEventDetails) => {
setOpen(isOpen);
setTriggerId(eventDetails.trigger?.id ?? null);
};
return (
<Tooltip.Provider>
<div className={styles.Container}>
<div className={styles.ButtonGroup}>
<Tooltip.Trigger className={styles.IconButton} handle={demoTooltip} id="trigger-1">
<InfoIcon aria-label="Information 1" className={styles.Icon} />
</Tooltip.Trigger>
<Tooltip.Trigger className={styles.IconButton} handle={demoTooltip} id="trigger-2">
<InfoIcon aria-label="Information 2" className={styles.Icon} />
</Tooltip.Trigger>
<Tooltip.Trigger className={styles.IconButton} handle={demoTooltip} id="trigger-3">
<InfoIcon aria-label="Information 3" className={styles.Icon} />
</Tooltip.Trigger>
</div>
<button
type="button"
className={styles.Button}
onClick={() => {
setTriggerId("trigger-2");
setOpen(true);
}}
>
Open programmatically
</button>
</div>
<Tooltip.Root handle={demoTooltip} open={open} onOpenChange={handleOpenChange} triggerId={triggerId}>
<Tooltip.Portal>
<Tooltip.Positioner sideOffset={10} className={styles.Positioner}>
<Tooltip.Popup className={styles.Popup}>
<Tooltip.Arrow className={styles.Arrow}>
<ArrowSvg />
</Tooltip.Arrow>
Controlled tooltip
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className={styles.ArrowFill} />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className={styles.ArrowOuterStroke} />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className={styles.ArrowInnerStroke} />
</svg>
);
}
function InfoIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" {...props}>
<circle cx="12" cy="12" r="10" />
<path d="M12 16v-4" />
<path d="M12 8h.01" />
</svg>
);
}
```
```css
/* demos.module.css */
.Container {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: center;
}
.ButtonGroup {
display: flex;
}
.IconButton {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
padding: 0;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
background-color: var(--color-gray-50);
color: var(--color-gray-900);
user-select: none;
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:only-child {
border-radius: 0.375rem;
}
&:first-child:not(:only-child) {
border-radius: 0.375rem 0 0 0.375rem;
}
&:last-child:not(:only-child) {
border-radius: 0 0.375rem 0.375rem 0;
}
&:not(:first-child) {
border-left: none;
}
}
.Icon {
width: 1.25rem;
height: 1.25rem;
}
.Positioner {
height: var(--positioner-height);
width: var(--positioner-width);
max-width: var(--available-width);
}
.Popup {
box-sizing: border-box;
font-size: 0.875rem;
line-height: 1.25rem;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
background-color: canvas;
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
&[data-instant] {
transition: none;
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
```
### Animating the Tooltip
You can animate a tooltip as it moves between different trigger elements.
This includes animating its position, size, and content.
#### Position and Size
To animate the tooltip's position, apply CSS transitions to the `left`, `right`, `top`, and `bottom` properties of the **Positioner** part.
To animate its size, transition the `width` and `height` of the **Popup** part.
#### Content
The tooltip also supports content transitions.
This is useful when different triggers display different content within the same tooltip.
To enable content animations, wrap the content in the `<Tooltip.Viewport>` part.
This part provides features to create direction-aware animations.
It renders a `div` with a `data-activation-direction` attribute (`left`, `right`, `up`, or `down`) that indicates the new trigger's position relative to the previous one.
Inside the `<Tooltip.Viewport>`, the content is further wrapped in `div`s with data attributes to help with styling:
- `data-current`: The currently visible content when no transitions are present or the incoming content.
- `data-previous`: The outgoing content during a transition.
You can use these attributes to style the enter and exit animations.
## Demo
### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Tooltip } from "@base-ui/react/tooltip";
import { ArrowSvg, InfoIcon, HelpIcon, AlertIcon } from "./icons-tw";
const demoTooltip = Tooltip.createHandle<React.ComponentType>();
export default function TooltipDetachedTriggersFullDemo() {
return (
<Tooltip.Provider>
<div className="flex">
<Tooltip.Trigger
className="
box-border flex size-10 items-center justify-center
border border-gray-200 rounded-l-md
bg-gray-50
text-base font-bold text-gray-900
select-none
data-popup-open:bg-gray-100
focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-600
hover:bg-gray-100
active:bg-gray-100"
handle={demoTooltip}
payload={InfoContent}
>
<InfoIcon aria-label="Information" className="size-5" />
</Tooltip.Trigger>
<Tooltip.Trigger
className="
box-border flex size-10 items-center justify-center
border-y border-r border-gray-200
bg-gray-50
text-base font-bold text-gray-900
select-none
data-popup-open:bg-gray-100
focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-600
hover:bg-gray-100
active:bg-gray-100"
handle={demoTooltip}
payload={HelpContent}
>
<HelpIcon aria-label="Help" className="size-5" />
</Tooltip.Trigger>
<Tooltip.Trigger
className="
box-border flex size-10 items-center justify-center
border-y border-r border-gray-200 rounded-r-md
bg-gray-50
text-base font-bold text-gray-900
select-none
data-popup-open:bg-gray-100
focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-600
hover:bg-gray-100
active:bg-gray-100"
handle={demoTooltip}
payload={AlertContent}
>
<AlertIcon aria-label="Alert" className="size-5" />
</Tooltip.Trigger>
</div>
<Tooltip.Root handle={demoTooltip}>
{({ payload: Payload }) => (
<Tooltip.Portal>
<Tooltip.Positioner
sideOffset={10}
className="
h-(--positioner-height) w-(--positioner-width)
max-w-(--available-width)
transition-[top,left,right,bottom,transform]
duration-[0.35s]
ease-[cubic-bezier(0.22,1,0.36,1)]
data-instant:transition-none"
>
<Tooltip.Popup
className="
relative
h-(--popup-height,auto) w-(--popup-width,auto)
max-w-[500px]
rounded-md
bg-[canvas]
text-sm
origin-(--transform-origin)
shadow-lg shadow-gray-200 outline-1 outline-gray-200
transition-[width,height,opacity,scale]
duration-[0.35s]
ease-[cubic-bezier(0.22,1,0.36,1)]
data-ending-style:opacity-0 data-ending-style:scale-90
data-instant:transition-none
data-starting-style:opacity-0 data-starting-style:scale-90
dark:shadow-none dark:outline-gray-300 dark:-outline-offset-1"
>
<Tooltip.Arrow
className="
flex
transition-[left]
duration-[0.35s]
ease-[cubic-bezier(0.22,1,0.36,1)]
data-instant:transition-none
data-[side=bottom]:-top-2 data-[side=bottom]:rotate-0
data-[side=left]:right-[-13px] data-[side=left]:rotate-90
data-[side=right]:left-[-13px] data-[side=right]:-rotate-90
data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
>
<ArrowSvg />
</Tooltip.Arrow>
<Tooltip.Viewport
className="
[--viewport-inline-padding:0.5rem]
relative
h-full w-full
overflow-clip
px-[var(--viewport-inline-padding)] py-1
[&_[data-previous]]:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding))]
[&_[data-previous]]:translate-x-0
[&_[data-previous]]:opacity-100
[&_[data-previous]]:transition-[translate,opacity]
[&_[data-previous]]:duration-[350ms,175ms]
[&_[data-previous]]:ease-[cubic-bezier(0.22,1,0.36,1)]
[&_[data-current]]:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding))]
[&_[data-current]]:translate-x-0
[&_[data-current]]:opacity-100
[&_[data-current]]:transition-[translate,opacity]
[&_[data-current]]:duration-[350ms,175ms]
[&_[data-current]]:ease-[cubic-bezier(0.22,1,0.36,1)]
data-[activation-direction~='left']:[&_[data-current][data-starting-style]]:-translate-x-1/2
data-[activation-direction~='left']:[&_[data-current][data-starting-style]]:opacity-0
data-[activation-direction~='right']:[&_[data-current][data-starting-style]]:translate-x-1/2
data-[activation-direction~='right']:[&_[data-current][data-starting-style]]:opacity-0
[[data-instant]_&_[data-previous]]:transition-none
[[data-instant]_&_[data-current]]:transition-none
data-[activation-direction~='left']:[&_[data-previous][data-ending-style]]:translate-x-1/2
data-[activation-direction~='left']:[&_[data-previous][data-ending-style]]:opacity-0
data-[activation-direction~='right']:[&_[data-previous][data-ending-style]]:-translate-x-1/2
data-[activation-direction~='right']:[&_[data-previous][data-ending-style]]:opacity-0"
>
{Payload !== undefined && <Payload />}
</Tooltip.Viewport>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
)}
</Tooltip.Root>
</Tooltip.Provider>
);
}
function InfoContent() {
return <span>This is information about the feature</span>;
}
function HelpContent() {
return <span>Need help?</span>;
}
function AlertContent() {
return <span>Warning: This action cannot be undone</span>;
}
```
```tsx
/* icons-tw.tsx */
import * as React from "react";
export function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className="fill-[canvas]" />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className="fill-gray-200 dark:fill-none" />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className="dark:fill-gray-300" />
</svg>
);
}
export function InfoIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" {...props}>
<circle cx="12" cy="12" r="10" />
<path d="M12 16v-4" />
<path d="M12 8h.01" />
</svg>
);
}
export function HelpIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" {...props}>
<circle cx="12" cy="12" r="10" />
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
<path d="M12 17h.01" />
</svg>
);
}
export function AlertIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
<path d="M12 9v4" />
<path d="M12 17h.01" />
</svg>
);
}
```
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Container {
display: flex;
}
.ButtonGroup {
display: flex;
}
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
padding: 0;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
background-color: var(--color-gray-50);
color: var(--color-gray-900);
font-weight: 700;
user-select: none;
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:only-child {
border-radius: 0.375rem;
}
&:first-child:not(:only-child) {
border-radius: 0.375rem 0 0 0.375rem;
}
&:last-child:not(:only-child) {
border-radius: 0 0.375rem 0.375rem 0;
}
&:not(:first-child) {
border-left: none;
}
}
.Icon {
width: 1.25rem;
height: 1.25rem;
}
.Positioner {
height: var(--positioner-height);
width: var(--positioner-width);
max-width: var(--available-width);
transition:
top 0.35s cubic-bezier(0.22, 1, 0.36, 1),
left 0.35s cubic-bezier(0.22, 1, 0.36, 1),
right 0.35s cubic-bezier(0.22, 1, 0.36, 1),
bottom 0.35s cubic-bezier(0.22, 1, 0.36, 1),
transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
&[data-instant] {
transition: none;
}
}
.Popup {
position: relative;
height: var(--popup-height, auto);
width: var(--popup-width, auto);
max-width: 500px;
transform-origin: var(--transform-origin);
border-radius: 0.375rem;
background-color: canvas;
font-size: 0.875rem;
line-height: 1.25rem;
transition:
width 0.35s cubic-bezier(0.22, 1, 0.36, 1),
height 0.35s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.35s cubic-bezier(0.22, 1, 0.36, 1),
transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
&[data-instant] {
transition: none;
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Arrow {
display: flex;
transition: left 0.35s cubic-bezier(0.22, 1, 0.36, 1);
&[data-instant] {
transition: none;
}
&[data-side="top"] {
bottom: -8px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -8px;
rotate: 0deg;
}
&[data-side="left"] {
right: -13px;
rotate: 90deg;
}
&[data-side="right"] {
left: -13px;
rotate: -90deg;
}
}
.ArrowFill {
fill: canvas;
}
.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}
.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}
.Viewport {
--viewport-inline-padding: 0.5rem;
position: relative;
height: 100%;
width: 100%;
overflow: clip;
padding: 0.25rem var(--viewport-inline-padding);
[data-previous],
[data-current] {
width: calc(var(--popup-width) - 2 * var(--viewport-inline-padding));
translate: 0;
opacity: 1;
transition:
translate 350ms cubic-bezier(0.22, 1, 0.36, 1),
opacity 175ms cubic-bezier(0.22, 1, 0.36, 1);
[data-instant] & {
transition: none;
}
}
&[data-activation-direction~="left"] [data-current][data-starting-style] {
translate: -50% 0;
opacity: 0;
}
&[data-activation-direction~="right"] [data-current][data-starting-style] {
translate: 50% 0;
opacity: 0;
}
&[data-activation-direction~="left"] [data-previous][data-ending-style] {
translate: 50% 0;
opacity: 0;
}
&[data-activation-direction~="right"] [data-previous][data-ending-style] {
translate: -50% 0;
opacity: 0;
}
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { Tooltip } from "@base-ui/react/tooltip";
import styles from "./index.module.css";
const demoTooltip = Tooltip.createHandle<React.ComponentType>();
export default function TooltipDetachedTriggersFullDemo() {
return (
<Tooltip.Provider>
<div className={styles.ButtonGroup}>
<Tooltip.Trigger className={styles.Button} handle={demoTooltip} payload={InfoContent}>
<InfoIcon aria-label="Information" className={styles.Icon} />
</Tooltip.Trigger>
<Tooltip.Trigger className={styles.Button} handle={demoTooltip} payload={HelpContent}>
<HelpIcon aria-label="Help" className={styles.Icon} />
</Tooltip.Trigger>
<Tooltip.Trigger className={styles.Button} handle={demoTooltip} payload={AlertContent}>
<AlertIcon aria-label="Alert" className={styles.Icon} />
</Tooltip.Trigger>
</div>
<Tooltip.Root handle={demoTooltip}>
{({ payload: Payload }) => (
<Tooltip.Portal>
<Tooltip.Positioner sideOffset={10} className={styles.Positioner}>
<Tooltip.Popup className={styles.Popup}>
<Tooltip.Arrow className={styles.Arrow}>
<ArrowSvg />
</Tooltip.Arrow>
<Tooltip.Viewport className={styles.Viewport}>{Payload !== undefined && <Payload />}</Tooltip.Viewport>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
)}
</Tooltip.Root>
</Tooltip.Provider>
);
}
function InfoContent() {
return <span>This is information about the feature</span>;
}
function HelpContent() {
return <span>Need help?</span>;
}
function AlertContent() {
return <span>Warning: This action cannot be undone</span>;
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className={styles.ArrowFill} />
<path d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z" className={styles.ArrowOuterStroke} />
<path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" className={styles.ArrowInnerStroke} />
</svg>
);
}
function InfoIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" {...props}>
<circle cx="12" cy="12" r="10" />
<path d="M12 16v-4" />
<path d="M12 8h.01" />
</svg>
);
}
function HelpIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" {...props}>
<circle cx="12" cy="12" r="10" />
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
<path d="M12 17h.01" />
</svg>
);
}
function AlertIcon(props: React.ComponentProps<"svg">) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
<path d="M12 9v4" />
<path d="M12 17h.01" />
</svg>
);
}
```
## API reference
### Provider
Provides a shared delay for multiple tooltips. The grouping logic ensures that
once a tooltip becomes visible, the adjacent tooltips will be shown instantly.
**Provider Props:**
| Prop | Type | Default | Description |
| :--------- | :---------- | :------ | :------------------------------------------------------------------------------------------------------------------------ |
| delay | `number` | - | How long to wait before opening a tooltip. Specified in milliseconds. |
| closeDelay | `number` | - | How long to wait before closing a tooltip. Specified in milliseconds. |
| timeout | `number` | `400` | Another tooltip will open instantly if the previous tooltip
is closed within this timeout. Specified in milliseconds. |
| children | `ReactNode` | - | - |
### Root
Groups all parts of the tooltip.
Doesn’t render its own HTML element.
**Root Props:**
| Prop | Type | Default | Description |
| :------------------------------------------------------- | :------------------------------------------------------------------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| defaultOpen | `boolean` | `false` | Whether the tooltip is initially open.To render a controlled tooltip, use the `open` prop instead. |
| open | `boolean` | - | Whether the tooltip is currently open. |
| onOpenChange | `((open: boolean, eventDetails: Tooltip.Root.ChangeEventDetails) => void)` | - | Event handler called when the tooltip is opened or closed. |
| actionsRef | `RefObject<Tooltip.Root.Actions \| null>` | - | A ref to imperative actions.\* `unmount`: Ends the closing phase of the tooltip after an externally controlled closing animation finishes.
Call `preventUnmountOnClose()` in `onOpenChange` first, otherwise the tooltip completes closing on its own.
Whether it leaves the DOM is decided by `keepMounted` on the portal. |
| \* `close`: Closes the tooltip imperatively when called. |
| defaultTriggerId | `string \| null` | - | ID of the trigger that the tooltip is associated with.
This is useful in conjunction with the `defaultOpen` prop to create an initially open tooltip. |
| handle | `Tooltip.Handle<Payload>` | - | A handle to associate the tooltip with a trigger.
If specified, allows external triggers to control the tooltip's open state.
Can be created with the Tooltip.createHandle() method. |
| onOpenChangeComplete | `((open: boolean) => void)` | - | Event handler called after any animations complete when the tooltip is opened or closed. |
| triggerId | `string \| null` | - | ID of the trigger that the tooltip is associated with.
This is useful in conjunction with the `open` prop to create a controlled tooltip.
There's no need to specify this prop when the tooltip is uncontrolled (i.e. when the `open` prop is not set). |
| trackCursorAxis | `'none' \| 'both' \| 'x' \| 'y'` | `'none'` | Determines which axis the tooltip should track the cursor on. |
| disabled | `boolean` | `false` | Whether the tooltip is disabled. |
| disableHoverablePopup | `boolean` | `false` | Whether the tooltip contents can be hovered without closing the tooltip. |
| children | `ReactNode \| PayloadChildRenderFunction<Payload>` | - | The content of the tooltip.
This can be a regular React node or a render function that receives the `payload` of the active trigger. |
### Trigger
An element to attach the tooltip to.
Renders a `<button>` element.
**Trigger Props:**
| Prop | Type | Default | Description |
| :--------- | :----------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| handle | `Tooltip.Handle<Payload>` | - | A handle to associate the trigger with a tooltip. |
| payload | `Payload` | - | A payload to pass to the tooltip when it is opened. |
| delay | `number` | `600` | How long to wait before opening the tooltip. Specified in milliseconds. |
| closeDelay | `number` | `0` | How long to wait before closing the tooltip. Specified in milliseconds. |
| className | `string \| ((state: Tooltip.Trigger.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Tooltip.Trigger.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Tooltip.Trigger.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Trigger Data Attributes:**
| Attribute | Type | Description |
| :-------------- | :--- | :---------------------------------------------- |
| data-popup-open | - | Present when the corresponding tooltip is open. |
### Portal
A portal element that moves the popup to a different part of the DOM.
By default, the portal element is appended to `<body>`.
Renders a `<div>` element.
**Portal Props:**
| Prop | Type | Default | Description |
| :---------- | :---------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| container | `HTMLElement \| ShadowRoot \| RefObject<HTMLElement \| ShadowRoot \| null> \| null` | - | A parent element to render the portal element into. |
| className | `string \| ((state: Tooltip.Portal.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Tooltip.Portal.State) => CSSProperties \| undefined)` | - | - |
| keepMounted | `boolean` | `false` | Whether to keep the portal mounted in the DOM while the popup is hidden. |
| render | `ReactElement \| ((props: HTMLProps, state: Tooltip.Portal.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
### Positioner
Positions the tooltip against the trigger.
Renders a `<div>` element.
**Positioner Props:**
| Prop | Type | Default | Description |
| :-------------------- | :------------------------- | :--------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| disableAnchorTracking | `boolean` | `false` | Whether to disable the popup from tracking any layout shift of its positioning anchor. |
| align | `Align` | `'center'` | How to align the popup relative to the specified side. |
| alignOffset | `number \| OffsetFunction` | `0` | Additional offset along the alignment axis in pixels.
Also accepts a function that returns the offset to read the dimensions of the anchor
and positioner elements, along with its side and alignment.The function takes a `data` object parameter with the following properties:\* `data.anchor`: the dimensions of the anchor element with properties `width` and `height`. |
- `data.positioner`: the dimensions of the positioner element with properties `width` and `height`.
- `data.side`: which side of the anchor element the positioner is aligned against.
- `data.align`: how the positioner is aligned relative to the specified side. |
| side | `Side` | `'top'` | Which side of the anchor element to align the popup against.
May automatically change to avoid collisions. |
| sideOffset | `number \| OffsetFunction` | `0` | Distance between the anchor and the popup in pixels.
Also accepts a function that returns the distance to read the dimensions of the anchor
and positioner elements, along with its side and alignment.The function takes a `data` object parameter with the following properties:\* `data.anchor`: the dimensions of the anchor element with properties `width` and `height`.
- `data.positioner`: the dimensions of the positioner element with properties `width` and `height`.
- `data.side`: which side of the anchor element the positioner is aligned against.
- `data.align`: how the positioner is aligned relative to the specified side. |
| arrowPadding | `number` | `5` | Minimum distance to maintain between the arrow and the edges of the popup.Use it to prevent the arrow element from hanging out of the rounded corners of a popup. |
| anchor | `Element \| RefObject<Element \| null> \| VirtualElement \| (() => Element \| VirtualElement \| null) \| null` | - | An element to position the popup against.
By default, the popup will be positioned against the trigger. |
| collisionAvoidance | `CollisionAvoidance` | - | Determines how to handle collisions when positioning the popup. |
| collisionBoundary | `Boundary` | `'clipping-ancestors'` | An element or a rectangle that delimits the area that the popup is confined to. |
| collisionPadding | `Padding` | `5` | Additional space to maintain from the edge of the collision boundary. |
| sticky | `boolean` | `false` | Whether to maintain the popup in the viewport after
the anchor element was scrolled out of view. |
| positionMethod | `'fixed' \| 'absolute'` | `'absolute'` | Determines which CSS `position` property to use. |
| className | `string \| ((state: Tooltip.Positioner.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Tooltip.Positioner.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Tooltip.Positioner.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Positioner Data Attributes:**
| Attribute | Type | Description |
| :----------------- | :------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| data-open | - | Present when the tooltip is open. |
| data-closed | - | Present when the tooltip is closed. |
| data-anchor-hidden | - | Present when the anchor is hidden. |
| data-align | `'start' \| 'center' \| 'end'` | Indicates how the popup is aligned relative to specified side. |
| data-side | `'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the popup is positioned relative to the trigger. |
**Positioner CSS Variables:**
| Variable | Type | Default | Description |
| :----------------- | :------- | :------ | :------------------------------------------------------------------------------------- |
| --anchor-height | `number` | - | The anchor's height. |
| --anchor-width | `number` | - | The anchor's width. |
| --available-height | `number` | - | The available height between the trigger and the edge of the viewport. |
| --available-width | `number` | - | The available width between the trigger and the edge of the viewport. |
| --transform-origin | `string` | - | The coordinates that this element is anchored to. Used for animations and transitions. |
### Popup
A container for the tooltip contents.
Renders a `<div>` element.
**Popup Props:**
| Prop | Type | Default | Description |
| :-------- | :--------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Tooltip.Popup.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Tooltip.Popup.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Tooltip.Popup.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Popup Data Attributes:**
| Attribute | Type | Description |
| :------------------ | :------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| data-open | - | Present when the tooltip is open. |
| data-closed | - | Present when the tooltip is closed. |
| data-align | `'start' \| 'center' \| 'end'` | Indicates how the popup is aligned relative to specified side. |
| data-instant | `'delay' \| 'dismiss' \| 'focus'` | Present if animations should be instant. |
| data-side | `'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the popup is positioned relative to the trigger. |
| data-starting-style | - | Present when the tooltip is animating in. |
| data-ending-style | - | Present when the tooltip is animating out. |
### Arrow
Displays an element positioned against the tooltip anchor.
Renders a `<div>` element.
**Arrow Props:**
| Prop | Type | Default | Description |
| :-------- | :--------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: Tooltip.Arrow.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component’s state. |
| style | `CSSProperties \| ((state: Tooltip.Arrow.State) => CSSProperties \| undefined)` | - | - |
| render | `ReactElement \| ((props: HTMLProps, state: Tooltip.Arrow.State) => ReactElement)` | - | Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a `ReactElement` or a function that returns the element to render. |
**Arrow Data Attributes:**
| Attribute | Type | Description |
| :-------------- | :------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| data-open | - | Present when the tooltip is open. |
| data-closed | - | Present when the tooltip is closed. |
| data-uncentered | - | Present when the tooltip arrow is uncentered. |
| data-align | `'start' \| 'center' \| 'end'` | Indicates how the popup is aligned relative to specified side. |
| data-instant | `'delay' \| 'dismiss' \| 'focus'` | Present if animations should be instant. |
| data-side | `'top' \| 'bottom' \| 'left' \| 'right' \| 'inline-end' \| 'inline-start'` | Indicates which side the popup is positioned relative to the trigger. |
references/use-render.md
---
title: useRender
subtitle: Hook for enabling a render prop in custom components.
description: Hook for enabling a render prop in custom components.
---
# useRender
Hook for enabling a render prop in custom components.
The `useRender` hook lets you build custom components that provide a `render` prop to override the default rendered element.
`render` supports lazy elements (v1.2.0).
## v1.4.1 notes
- Tabbability with `display: contents` was fixed in the `1.4.1` line. If your custom `render` wrappers flatten DOM structure, re-test keyboard navigation instead of assuming old workarounds are still needed.
## v1.4.0 notes
- Warning messages around incorrect `render` prop usage are more accurate, which helps when debugging wrapper components.
- `event.preventBaseUIHandler()` runtime wrapping was fixed; keep using it only inside merged synthetic event handlers where you intentionally want to block Base UI's internal behavior.
## Examples
A `render` prop for a custom Text component lets consumers use it to replace the default rendered `p` element with a different tag or component.
## Demo
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Text {
font-size: 0.875rem;
line-height: 1rem;
color: var(--color-gray-900);
strong& {
font-weight: 500;
}
}
```
```tsx
/* index.tsx */
"use client";
import { useRender } from "@base-ui/react/use-render";
import { mergeProps } from "@base-ui/react/merge-props";
import styles from "./index.module.css";
interface TextProps extends useRender.ComponentProps<"p"> {}
function Text(props: TextProps) {
const { render, ...otherProps } = props;
const element = useRender({
defaultTagName: "p",
render,
props: mergeProps<"p">({ className: styles.Text }, otherProps),
});
return element;
}
export default function ExampleText() {
return (
<div>
<Text>Text component rendered as a paragraph tag</Text>
<Text render={<strong />}>Text component rendered as a strong tag</Text>
</div>
);
}
```
The callback version of the `render` prop enables more control of how props are spread, and also passes the internal `state` of a component.
## Demo
### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
& span {
font-variant-numeric: tabular-nums;
display: inline-block;
text-align: end;
min-width: 2.5ch;
}
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.suffix {
margin-left: 0.125rem;
}
```
```tsx
/* index.tsx */
"use client";
import * as React from "react";
import { useRender } from "@base-ui/react/use-render";
import { mergeProps } from "@base-ui/react/merge-props";
import styles from "./index.module.css";
interface CounterState {
odd: boolean;
}
interface CounterProps extends useRender.ComponentProps<"button", CounterState> {}
function Counter(props: CounterProps) {
const { render, ...otherProps } = props;
const [count, setCount] = React.useState(0);
const odd = count % 2 === 1;
const state = React.useMemo(() => ({ odd }), [odd]);
const defaultProps: useRender.ElementProps<"button"> = {
className: styles.Button,
type: "button",
children: (
<React.Fragment>
Counter: <span>{count}</span>
</React.Fragment>
),
onClick() {
setCount((prev) => prev + 1);
},
"aria-label": `Count is ${count}, click to increase.`,
};
const element = useRender({
defaultTagName: "button",
render,
state,
props: mergeProps<"button">(defaultProps, otherProps),
});
return element;
}
export default function ExampleCounter() {
return (
<Counter
render={(props, state) => (
<button {...props}>
{props.children}
<span className={styles.suffix}>{state.odd ? "👎" : "👍"}</span>
</button>
)}
/>
);
}
```
## Merging props
The `mergeProps` function merges two or more sets of React props together. It safely merges three types of props:
1. Event handlers, so that all are invoked
2. `className` strings
3. `style` properties
`mergeProps` merges objects from left to right, so that subsequent objects' properties in the arguments overwrite previous ones. Merging props is useful when creating custom components, as well as inside the callback version of the `render` prop for any Base UI component.
```tsx title="Using mergeProps in the render callback"
import { mergeProps } from "@base-ui/react/merge-props";
import styles from "./index.module.css";
function Button() {
return (
<Component
render={(props, state) => (
<button
{...mergeProps<"button">(props, {
className: styles.Button,
})}
/>
)}
/>
);
}
```
## Merging refs
When building custom components, you often need to control a ref internally while still letting external consumers pass their own—merging refs lets both parties have access to the underlying DOM element. The `ref` option in `useRender` enables this, which holds an array of refs to be merged together.
In React 19, `React.forwardRef()` is not needed when building primitive components, as the external ref prop is already contained inside `props`. Your internal ref can be passed to `ref` to be merged with `props.ref`:
```tsx title="React 19" {6} "internalRef"
function Text({ render, ...props }: TextProps) {
const internalRef = React.useRef<HTMLElement | null>(null);
const element = useRender({
defaultTagName: "p",
ref: internalRef,
props,
render,
});
return element;
}
```
In older versions of React, you need to use `React.forwardRef()` and add the forwarded ref to the `ref` array along with your own internal ref.
The [examples](/react/utils/use-render.md) above assume React 19, and should be modified to use `React.forwardRef()` to support React 18 and 17.
```tsx title="React 18 and 17" {9} "forwardedRef" "internalRef"
const Text = React.forwardRef(function Text({ render, ...props }: TextProps, forwardedRef: React.ForwardedRef<HTMLElement>) {
const internalRef = React.useRef<HTMLElement | null>(null);
const element = useRender({
defaultTagName: "p",
ref: [forwardedRef, internalRef],
props,
render,
});
return element;
});
```
## TypeScript
To type props, there are two interfaces:
- `useRender.ComponentProps` for a component's external (public) props. It types the `render` prop and HTML attributes.
- `useRender.ElementProps` for the element's internal (private) props. It types HTML attributes alone.
```tsx title="Typing props" {1,4}
interface ButtonProps extends useRender.ComponentProps<"button"> {}
function Button({ render, ...props }: ButtonProps) {
const defaultProps: useRender.ElementProps<"button"> = {
className: styles.Button,
type: "button",
children: "Click me",
};
const element = useRender({
defaultTagName: "button",
render,
props: mergeProps<"button">(defaultProps, props),
});
return element;
}
```
## Migrating from Radix UI
Radix UI uses an `asChild` prop, while Base UI uses a `render` prop. Learn more about how composition works in Base UI in the [composition guide](/react/handbook/composition.md).
In Radix UI, the `Slot` component lets you implement an `asChild` prop.
```jsx title="Radix UI Slot component"
import { Slot } from "radix-ui";
function Button({ asChild, ...props }) {
const Comp = asChild ? Slot.Root : "button";
return <Comp {...props} />;
}
// Usage
<Button asChild>
<a href="/contact">Contact</a>
</Button>;
```
In Base UI, `useRender` lets you implement a `render` prop. The example below is the equivalent implementation to the Radix example above.
```jsx title="Base UI render prop"
import { useRender } from "@base-ui/react/use-render";
function Button({ render, ...props }) {
return useRender({
defaultTagName: "button",
render,
props,
});
}
// Usage
<Button render={<a href="/contact" />}>Contact</Button>;
```
## API reference
### Input parameters
**Props:**
| Prop | Type | Default | Description |
| :--------------------- | :------------------------------------------------------------------- | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| defaultTagName | `keyof React.JSX.IntrinsicElements` | - | The default tag name to use for the rendered element when `render` is not provided. |
| render | `RenderProp<State>` | - | The React element or a function that returns one to override the default element. |
| props | `Record<string, unknown>` | - | Props to be spread on the rendered element.They are merged with the internal props
of the component, so that event handlers are merged,
`className` strings and `style` properties are joined, and other external props overwrite the internal ones. |
| ref | `React.Ref<RenderedElementType> \| React.Ref<RenderedElementType>[]` | - | The refs to apply to the rendered element. |
| state | `State` | - | The state of the component. It will be used as a parameter for the render callback. |
| stateAttributesMapping | `StateAttributesMapping<State>` | - | Custom mapping for converting state properties to data-\* attributes. |
### Return value
**Return Value:**
| Property | Type | Description |
| :------- | :------------------- | :------------------------- |
| element | `React.ReactElement` | The rendered React element |
```tsx title="Usage"
const element = useRender({
// Input parameters
});
```
SKILL.md
---
name: base-ui
description: "Base UI unstyled React components. Covers forms, menus, overlays, composition. Use when building accessible, unstyled React UI components with @base-ui/react, composing with render props, or implementing custom-styled form controls, menus, and overlays. Keywords: @base-ui/react, render props, unstyled components."
metadata:
version: "1.9.0"
release_date: "2026-10-09"
---
# Base UI (React)
Base UI provides unstyled, composable React components. This skill focuses on assembling multi-part components, composing with the `render` prop, and meeting accessibility requirements while you supply styling.
## Quick navigation (references)
### Utilities
- references/csp-provider.md
- references/direction-provider.md
- references/merge-props.md
- references/use-render.md
### Forms & inputs
- references/form.md
- references/fieldset.md
- references/input.md
- references/number-field.md
- references/otp-field.md
- references/radio.md
- references/select.md
- references/slider.md
- references/switch.md
### Feedback & status
- references/meter.md
- references/progress.md
- references/toast.md
### Menus, navigation, overlays
- references/drawer.md
- references/menu.md
- references/menubar.md
- references/navigation-menu.md
- references/popover.md
- references/preview-card.md
- references/tabs.md
- references/tooltip.md
### Toggles & controls
- references/toggle.md
- references/toggle-group.md
- references/toolbar.md
### Layout & separators
- references/scroll-area.md
- references/separator.md
## Release Highlights (1.9.0)
- **Menu filtering (preview)**: `<Menu.FilterProvider>` wraps a `<Menu.Root>` or `<Menu.SubmenuRoot>`, `<Menu.Input>` takes the query, and `<Menu.List>` holds the items that get filtered. `<Menu.Clear>` and `<Menu.Empty>` cover reset and no-match states. Full details in references/menu.md.
- **Unmount control**: automatic unmounting is decoupled from `actionsRef`. Call `eventDetails.preventUnmountOnClose()` inside `onOpenChange` to keep the popup mounted, then end the closing phase yourself with `actionsRef.current.unmount()`. Applies to Autocomplete, Combobox, Menu, Select, Dialog, Alert Dialog, Popover, Tooltip, and Preview Card. Details in references/popover.md.
- `Menu.Root` accepts `onItemHighlighted(highlightedItem, eventDetails)`. The reason is `'keyboard'`, `'pointer'`, or `'none'`; pointer events may be a `MouseEvent` rather than a `PointerEvent`, and typing reports `'none'`.
- `Autocomplete` and `Combobox` support `autoHighlight` with asynchronous results, accept readonly arrays for multiple values, and report the originating event to `onItemHighlighted`.
- `Menu` fixes nested menus without a root trigger, submenu closing when focus returns through its guard, repeated trigger activation toggling submenus, and the focus-guard loop when the trigger is the only tabbable element.
- `Drawer` preserves swipe release velocity on 120 Hz input and after a trailing stationary pointer event, `data-base-ui-swipe-ignore` gains `x` and `y` options, and a drag starting on an SVG scrolls instead of swiping.
- `Accordion.Trigger` and `Collapsible.Trigger` apply the native `disabled` attribute when rendering native buttons, and `hiddenUntilFound` panels expand synchronously on `beforematch`.
- `Radio Group` and `Checkbox` prefer `aria-label` over an associated label, ignored arrow keys no longer select on refocus, and `inputRef` forwarding is fixed.
- `NumberField.ScrubArea` fires `onClick` once per click, and `OTPField` no longer over-fills slots during Safari IME composition.
- `Field.Control` no longer warns when an uncontrolled `defaultValue` changes, `ScrollArea` drops the presentational viewport role, and `Tabs` can select tabs with function values.
- The `CssVariables` and `DataAttributes` types are now exported.
## v1.7.0 - v1.8.0 highlights
- `Combobox` adds a `createItems` collection API for building the option collection programmatically.
- `Avatar.Image` adds a `keepMounted` prop so the image subtree stays mounted (useful for avoiding remount flashes).
- `Toast` accepts functional updates derived from the current toast, and the `render` prop is honored in `Toast.Title`, `Toast.Description`, and `Toast.Action`.
- `Autocomplete` and `Combobox` emit `input-press` and `cancel-open` change event details; inline `Combobox` exposes an `expanded` state.
- `Combobox.Item` and `Select.Item` inherit the disabled state from their root.
- `Form` focuses the first invalid field in document order; `Field` publishes neutral validity while async validation is in flight.
- Mostly accessibility fixes, popup/overlay behavior fixes, and bundle-size reductions across drawers, menus, tooltips, and scroll areas.
## v1.4.1 highlights
- `mergeProps` now forwards multi-argument event handlers correctly, which matters for wrapped callbacks that receive extra details/state alongside the event.
- Tabbability with `display: contents` was fixed; retest custom wrappers and render-prop compositions that flatten DOM structure.
- Popup sizing on rapid trigger hover was fixed, and clipped-item highlight clearing inside scroll containers is more reliable.
## v1.6.0 highlights
- `OTPFieldPreview` namespace is renamed to `OTPField`; update imports that referenced the preview namespace.
- Broad behavioral bug fixes across Accordion, Alert Dialog, Autocomplete, Avatar, Checkbox, Dialog, Drawer, Field, Menu, Number Field, OTP Field, and other components.
## v1.5.0 highlights
- `OTPField` replaces `sanitizeValue()` with `normalizeValue()` and improves keyboard shortcut plus validation-composition behavior.
- Popup-based components now respect controlled `open` state more consistently, which affects `Popover`, `Menu`, `PreviewCard`, and related overlays.
- `Form`, `Field`, and `OTPField` avoid older `flushSync`-style validation timing patterns; retest any local validation scheduling workarounds before keeping them.
- RTL behavior is corrected across `NavigationMenu`, `Popover`, and `ScrollArea`.
- `Drawer.Viewport` now forwards `style`, which is useful for custom viewport sizing and animated layout constraints.
## v1.4.0 highlights
- `OTPField` arrives as a preview primitive for one-time-password and verification-code entry.
- Hidden inputs expose `form` and `suppressHydrationWarning`, which matters when controls submit to external forms or hydrate across SSR boundaries.
- `render` warnings and `preventBaseUIHandler()` runtime behavior are more reliable in composed components.
- Overlay/input primitives received important behavioral fixes across `Drawer`, `NavigationMenu`, `Select`, `Slider`, `Toast`, and `Switch`.
## v1.3.0 highlights
- Drawer is now stable (no longer preview) and should be treated as a first-class overlay primitive.
- Autocomplete and Combobox add `InputGroup`; Combobox, Select, and Slider add `Label` parts.
- Drawer adds `SwipeArea` for explicit swipe affordances.
- Tooltip adds `closeOnClick`; Toast can close all toasts.
- Checkbox, Radio Group, and Switch improve automatic `aria-labelledby` support.
- `render` prop support from v1.2.0 remains relevant, including lazy elements.
## Core concepts to apply
- **Unstyled primitives**: all visuals come from your CSS/Tailwind/CSS Modules. Base UI handles behavior and accessibility.
- **Multi-part anatomy**: most components follow `Root` + subparts (`Trigger`, `Popup`, `Item`, etc.). Assemble them explicitly.
- **`render` prop**: replace the default element or wrap with your own component; when using the function form, merge props manually with `mergeProps`.
- **State-driven styling**: many parts expose data attributes such as `data-[popup-open]`, `data-[highlighted]`, `data-[pressed]`, `data-[checked]`, `data-[invalid]`, etc.
- **Portals for overlays**: use `Portal` + `Positioner` + `Popup` for popovers, menus, tooltips, select, navigation menu content, and preview cards.
## Recipes
### 1) Build accessible forms quickly
- Wrap inputs with `Form` for submission and error aggregation.
- Use `Field.Root` with a `name`, then `Field.Label`, `Field.Control`, and `Field.Error` for labels and errors.
- Group related fields with `Fieldset.Root` + `Fieldset.Legend`.
- For `Radio` groups, either use `RadioGroup` with `aria-labelledby`, or render `RadioGroup` via `Fieldset.Root` and use `Field.Label` for each option.
- Ensure **every** form control has an accessible name (label or `Field`/`Fieldset` labeling pattern).
### 2) Compose with `render` + `mergeProps`
- When passing a React element to `render`, Base UI merges props automatically.
- When passing a function to `render`, **merge** Base UI props and your props with `mergeProps`.
- Use `event.preventBaseUIHandler()` inside merged event handlers to stop Base UI’s internal behavior when needed (synthetic events only).
### 3) Overlays and menus (portaled UI)
- Build popups using `Root` + `Trigger` + `Portal` + `Positioner` + `Popup`.
- For menus and navigation, add `Item`, `Separator`, `Arrow`, and list/viewport parts as needed.
- `NavigationMenu.Link` supports `render` for framework router links.
- To drive the close animation from an external library, call `eventDetails.preventUnmountOnClose()` in `onOpenChange` and finish with `actionsRef.current.unmount()`; `keepMounted` on the portal decides whether the DOM node stays.
### 4) Toasts
- Wrap the app (or a subtree) with `Toast.Provider`.
- Use `Toast.useToastManager()` to create toasts.
- Render a `Toast.Viewport` inside `Toast.Portal`, then map `toasts` to `Toast.Root` entries with `Toast.Content`, `Toast.Title`, `Toast.Description`, and `Toast.Close`.
### 5) RTL and CSP constraints
- For RTL behavior, wrap with `DirectionProvider` and also set `dir="rtl"` (provider does not set the DOM direction).
- For strict CSP, wrap with `CSPProvider` and pass `nonce`, or disable inline style elements if your app supplies external styles. Beware inline style **attributes** are not controlled by `CSPProvider`.
### 6) Sliders and ranges
- Single value sliders: one `Slider.Thumb`.
- Range sliders: pass an array and render a `Slider.Thumb` per value; add `index` for SSR alignment.
- Adjust thumb alignment with `thumbAlignment` when edge alignment is required.
## Critical prohibitions
- Do not omit accessible labels for inputs, sliders, switches, or radio groups.
- Do not expect Base UI to provide default styling; apply your own styles.
- Do not forget `Portal` for overlays that need to escape stacking/overflow contexts.
- Do not skip `mergeProps` when using the function form of `render`.
## Output expectations
When responding to the user, provide:
- The specific Base UI components to use (and their key parts).
- A brief assembly plan (Root + parts + portal/positioner where relevant).
- A short checklist for accessibility and state-driven styling.
## Links
- [Documentation](https://base-ui.com/)
- [Releases](https://github.com/mui/base-ui/releases)
- [GitHub](https://github.com/mui/base-ui)
- [npm](https://www.npmjs.com/package/@base-ui-components/react)