
Base Ui
- 196 installs
- 22 repo stars
- Updated August 1, 2026
- itechmeat/llm-code
Implement accessible, unstyled headless UI primitives with Base UI in React apps, composing custom styled components without fighting opinionated design systems.
About
Teaches agents to build React frontends with Base UI headless primitives, composing accessible dialogs, menus, popovers, and form controls with project-specific styling instead of a bundled visual theme.
- Headless accessible React primitives
- Custom styling without design lock-in
- Dialogs, menus, popovers, and form controls
- Composable component architecture
- Pairs with Tailwind or CSS modules
Base Ui by the numbers
- 196 all-time installs (skills.sh)
- +8 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #860 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/itechmeat/llm-code --skill base-uiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 196 |
|---|---|
| repo stars | ★ 22 |
| Last updated | August 1, 2026 |
| Repository | itechmeat/llm-code ↗ |
What it does
Implement accessible, unstyled headless UI primitives with Base UI in React apps, composing custom styled components without fighting opinionated design systems.
Files
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
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:
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.
<style> .base-ui-disable-scrollbar { scrollbar-width: none; } .base-ui-disable-scrollbar::-webkit-scrollbar { display: none; } </style>
Specify `disableStyleElements` to remove these tags:
<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` | - | - |
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.
/* 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.
/* 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);
}
}/* 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. |
Drawer
Base UI Drawer is a slide-in overlay primitive for mobile navigation, sheets, and bottom panels.
v1.3.0 status
Draweris 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.SwipeAreais available when you want an explicit edge swipe affordance.
Recommended anatomy
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:
Triggerfor click/tap,SwipeAreafor 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.Viewportnow forwardsstyle. Prefer that over wrapper div hacks when you need inline max-height, sizing, or animated viewport constraints.
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.
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.
/* 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.
/* 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);
}/* 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. |
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
formprop, 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
flushSyncbehavior in the1.5.0line. Remove timing hacks only after retesting real submit and error-render paths.
Demo
Tailwind
This example shows how to implement the component using Tailwind CSS.
/* 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.
/* 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);
}
}/* 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. 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.
/ 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.
/ 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); } }
/ 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.
<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.
/ 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.
/ 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); } }
/ 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. |
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.
/* 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.
/* 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;
}
}/* 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 theFieldcomponent. See the forms guide.
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). |
Menubar
A menu bar providing commands and options for your application.
Demo
Tailwind
This example shows how to implement the component using Tailwind CSS.
/* 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.
/* 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);
}/* 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. |
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:mergeProps({ ref: refA }, { ref: refB });
- `className` values are concatenated right-to-left (rightmost first):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):
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:
onClick(event) { event.preventBaseUIHandler(); // Your custom behavior here }
## Demo
### CSS Modules
This example shows how to implement the component using CSS Modules.
/ 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; } }
/ 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:
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. |
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.
/* 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.
/* 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;
}/* 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. |
OTP Field
Base UI OTPField is a preview primitive for one-time-password and verification-code entry.
Import and anatomy
import { OTPFieldPreview as OTPField } from "@base-ui/react/otp-field";
<OTPField.Root length={6}>
<OTPField.Input />
<OTPField.Separator />
</OTPField.Root>;Core parts
OTPField.Rootmanages the overall value, validation, and form integration.OTPField.Inputrenders each character slot.OTPField.Separatoris a visual grouping element for layouts such as123-456.
Practical guidance
- Treat it as preview API surface until Base UI marks it stable.
- Always provide an accessible name via a native
<label>orFieldcomposition. - Use
validationType="numeric"for standard verification codes; switch toalphanumericonly when the backend really expects mixed characters. - Use
onValueCompletefor side effects like verification requests, andautoSubmitonly when the owning form can safely submit as soon as the value is complete. - For custom input cleanup rules, set
validationType="none"and providenormalizeValue.
v1.5.0 notes
sanitizeValue()was renamed tonormalizeValue().- Keyboard shortcuts such as Ctrl/Cmd editing behave more reliably.
- Prefer one normalization path plus explicit validation instead of layering custom sanitizers.
Operational notes
autoComplete="one-time-code"is the default and should usually be preserved for mobile autofill flows.form,required,readOnly,disabled, andnamelive onRoot, so form semantics stay centralized.maskis useful for short-lived verification secrets, but verify that the UX still supports paste and correction clearly.
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.
/* 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.
/* 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;
}/* 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. |
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.
/* 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.
/* 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;
}
}/* 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. |