\| ((closeType: InteractionType) => boolean \| void \| HTMLElement \| null)` | - | Determines the element to focus when the dialog is closed. `false`: Do not move focus.`true`: Move focus based on the default behavior (trigger or previously focused element).`RefObject`: Move focus to the ref element.`function`: Called with the interaction type (`mouse`, `touch`, `pen`, or `keyboard`).
Return an element to focus, `true` to use the default behavior, `null` to fall back to the default behavior, or `false`/`undefined` to do nothing. |
| className | `string \| ((state: AlertDialog.Popup.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component's state. |
| style | `React.CSSProperties \| ((state: AlertDialog.Popup.State) => React.CSSProperties \| undefined)` | - | Style applied to the element, or a function that
returns a style object based on the component's state. |
| render | `ReactElement \| ((props: HTMLProps, state: AlertDialog.Popup.State) => ReactElement)` | - | Allows you to replace the component's HTML element
with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
**Popup Data Attributes:**
| Attribute | Type | Description |
| :---------------------- | :--- | :--------------------------------------------------------------- |
| data-open | - | Present when the dialog is open. |
| data-closed | - | Present when the dialog is closed. |
| data-nested | - | Present when the dialog is nested within another dialog. |
| data-nested-dialog-open | - | Present when the dialog has other open dialogs nested within it. |
| data-starting-style | - | Present when the dialog is animating in. |
| data-ending-style | - | Present when the dialog is animating out. |
**Popup CSS Variables:**
| Variable | Type | Description |
| :----------------- | :------- | :-------------------------------------------- |
| `--nested-dialogs` | `number` | Indicates how many dialogs are nested within. |
##### Popup.Props
Re-export of [Popup](#alert-dialog) props.
##### Popup.State
```typescript
type AlertDialogPopupState = {
/** Whether the dialog is currently open. */
open: boolean;
/** The transition status of the component. */
transitionStatus: TransitionStatus;
/** Whether the dialog is nested within a parent dialog. */
nested: boolean;
/** Whether the dialog has nested dialogs open. */
nestedDialogOpen: boolean;
};
```
##### Title
A heading that labels the dialog.
Renders an `` element.
**Title Props:**
| Prop | Type | Default | Description |
| :-------- | :---------------------------------------------------------------------------------------------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: AlertDialog.Title.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component's state. |
| style | `React.CSSProperties \| ((state: AlertDialog.Title.State) => React.CSSProperties \| undefined)` | - | Style applied to the element, or a function that
returns a style object based on the component's state. |
| render | `ReactElement \| ((props: HTMLProps, state: AlertDialog.Title.State) => ReactElement)` | - | Allows you to replace the component's HTML element
with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
##### Title.Props
Re-export of [Title](#alert-dialog) props.
##### Title.State
```typescript
type AlertDialogTitleState = {};
```
##### Description
A paragraph with additional information about the dialog.
Renders a ` ` element.
**Description Props:**
| Prop | Type | Default | Description |
| :-------- | :---------------------------------------------------------------------------------------------------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: AlertDialog.Description.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component's state. |
| style | `React.CSSProperties \| ((state: AlertDialog.Description.State) => React.CSSProperties \| undefined)` | - | Style applied to the element, or a function that
returns a style object based on the component's state. |
| render | `ReactElement \| ((props: HTMLProps, state: AlertDialog.Description.State) => ReactElement)` | - | Allows you to replace the component's HTML element
with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
##### Description.Props
Re-export of [Description](#alert-dialog) props.
##### Description.State
```typescript
type AlertDialogDescriptionState = {};
```
##### Close
A button that closes the dialog.
Renders a `` element.
**Close Props:**
| Prop | Type | Default | Description |
| :----------- | :---------------------------------------------------------------------------------------------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| nativeButton | `boolean` | `true` | Whether the component renders a native `` element when replacing it
via the `render` prop.
Set to `false` if the rendered element is not a button (for example, ``). |
| className | `string \| ((state: AlertDialog.Close.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component's state. |
| style | `React.CSSProperties \| ((state: AlertDialog.Close.State) => React.CSSProperties \| undefined)` | - | Style applied to the element, or a function that
returns a style object based on the component's state. |
| render | `ReactElement \| ((props: HTMLProps, state: AlertDialog.Close.State) => ReactElement)` | - | Allows you to replace the component's HTML element
with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
**Close Data Attributes:**
| Attribute | Type | Description |
| :------------ | :--- | :----------------------------------- |
| data-disabled | - | Present when the button is disabled. |
##### Close.Props
Re-export of [Close](#alert-dialog) props.
##### Close.State
```typescript
type AlertDialogCloseState = {
/** Whether the button is currently disabled. */
disabled: boolean;
};
```
##### Viewport
A positioning container for the dialog popup that can be made scrollable.
Renders a `
` element.
**Viewport Props:**
| Prop | Type | Default | Description |
| :-------- | :------------------------------------------------------------------------------------------------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: AlertDialog.Viewport.State) => string \| undefined)` | - | CSS class applied to the element, or a function that
returns a class based on the component's state. |
| style | `React.CSSProperties \| ((state: AlertDialog.Viewport.State) => React.CSSProperties \| undefined)` | - | Style applied to the element, or a function that
returns a style object based on the component's state. |
| render | `ReactElement \| ((props: HTMLProps, state: AlertDialog.Viewport.State) => ReactElement)` | - | Allows you to replace the component's HTML element
with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
**Viewport Data Attributes:**
| Attribute | Type | Description |
| :---------------------- | :--- | :--------------------------------------------------------------- |
| data-open | - | Present when the dialog is open. |
| data-closed | - | Present when the dialog is closed. |
| data-nested | - | Present when the dialog is nested within another dialog. |
| data-nested-dialog-open | - | Present when the dialog has other open dialogs nested within it. |
| data-starting-style | - | Present when the dialog is animating in. |
| data-ending-style | - | Present when the dialog is animating out. |
##### Viewport.Props
Re-export of [Viewport](#alert-dialog) props.
##### Viewport.State
```typescript
type AlertDialogViewportState = {
/** Whether the dialog is currently open. */
open: boolean;
/** The transition status of the component. */
transitionStatus: TransitionStatus;
/** Whether the dialog is nested within another dialog. */
nested: boolean;
/** Whether the dialog has nested dialogs open. */
nestedDialogOpen: boolean;
};
```
##### createHandle
Creates a new handle to connect an AlertDialog.Root with detached AlertDialog.Trigger components.
**Return Value:**
```tsx
type ReturnValue = AlertDialog.Handle
;
```
##### Handle
A handle to control an Alert Dialog imperatively and to associate detached triggers with it.
**Constructor Parameters:**
| Parameter | Type | Default | Description |
| :-------- | :--------------------- | :------ | :--------------------------------------- |
| store? | `DialogStore` | - | Internal store holding the dialog state. |
**Properties:**
| Property | Type | Modifiers | Description |
| :------- | :-------- | :-------- | :---------------------------------------------- |
| isOpen | `boolean` | readonly | Indicates whether the dialog is currently open. |
**Methods:**
```typescript
function open(triggerId: string | null): void;
```
Opens the dialog and associates it with the trigger with the given id.
The trigger, if provided, must be a matching Trigger component with this handle passed as a prop.
This method should only be called in an event handler or an effect (not during rendering).
```typescript
function openWithPayload(payload: Payload): void;
```
Opens the dialog and sets the payload.
Does not associate the dialog with any trigger.
```typescript
function close(): void;
```
Closes the dialog.
#### External Types
##### PayloadChildRenderFunction
```typescript
type PayloadChildRenderFunction = (arg: { payload: unknown | undefined }) => ReactNode;
```
##### preventUnmountOnClose
```typescript
type preventUnmountOnClose = () => void;
```
##### InteractionType
```typescript
type InteractionType = 'mouse' | 'touch' | 'pen' | 'keyboard' | '';
```
#### Export Groups
- `AlertDialog.Root`: `AlertDialog.Root`, `AlertDialog.Root.State`, `AlertDialog.Root.Props`, `AlertDialog.Root.Actions`, `AlertDialog.Root.ChangeEventReason`, `AlertDialog.Root.ChangeEventDetails`
- `AlertDialog.Backdrop`: `AlertDialog.Backdrop`, `AlertDialog.Backdrop.Props`, `AlertDialog.Backdrop.State`
- `AlertDialog.Close`: `AlertDialog.Close`, `AlertDialog.Close.Props`, `AlertDialog.Close.State`
- `AlertDialog.Description`: `AlertDialog.Description`, `AlertDialog.Description.Props`, `AlertDialog.Description.State`
- `AlertDialog.Popup`: `AlertDialog.Popup`, `AlertDialog.Popup.Props`, `AlertDialog.Popup.State`
- `AlertDialog.Portal`: `AlertDialog.Portal`, `AlertDialog.Portal.State`, `AlertDialog.Portal.Props`
- `AlertDialog.Title`: `AlertDialog.Title`, `AlertDialog.Title.Props`, `AlertDialog.Title.State`
- `AlertDialog.Trigger`: `AlertDialog.Trigger`, `AlertDialog.Trigger.Props`, `AlertDialog.Trigger.State`
- `AlertDialog.Viewport`: `AlertDialog.Viewport`, `AlertDialog.Viewport.State`, `AlertDialog.Viewport.Props`
- `AlertDialog.Handle`
- `AlertDialog.createHandle`
- `Default`: `AlertDialogBackdropProps`, `AlertDialogBackdropState`, `AlertDialogCloseProps`, `AlertDialogCloseState`, `AlertDialogDescriptionProps`, `AlertDialogDescriptionState`, `AlertDialogPopupProps`, `AlertDialogPopupState`, `AlertDialogPortalProps`, `AlertDialogPortalState`, `AlertDialogTitleProps`, `AlertDialogTitleState`, `AlertDialogViewportProps`, `AlertDialogViewportState`, `AlertDialogRootState`, `AlertDialogRootProps`, `AlertDialogRootActions`, `AlertDialogRootChangeEventReason`, `AlertDialogRootChangeEventDetails`, `AlertDialogTriggerProps`, `AlertDialogTriggerState`
#### Canonical Types
Maps `Canonical`: `Alias` — Use Canonical when its namespace is already imported; otherwise use Alias.
- `AlertDialog.Root.State`: `AlertDialogRootState`
- `AlertDialog.Root.Props`: `AlertDialogRootProps`
- `AlertDialog.Root.Actions`: `AlertDialogRootActions`
- `AlertDialog.Root.ChangeEventReason`: `AlertDialogRootChangeEventReason`
- `AlertDialog.Root.ChangeEventDetails`: `AlertDialogRootChangeEventDetails`
- `AlertDialog.Backdrop.Props`: `AlertDialogBackdropProps`
- `AlertDialog.Backdrop.State`: `AlertDialogBackdropState`
- `AlertDialog.Close.Props`: `AlertDialogCloseProps`
- `AlertDialog.Close.State`: `AlertDialogCloseState`
- `AlertDialog.Description.Props`: `AlertDialogDescriptionProps`
- `AlertDialog.Description.State`: `AlertDialogDescriptionState`
- `AlertDialog.Popup.Props`: `AlertDialogPopupProps`
- `AlertDialog.Popup.State`: `AlertDialogPopupState`
- `AlertDialog.Portal.State`: `AlertDialogPortalState`
- `AlertDialog.Portal.Props`: `AlertDialogPortalProps`
- `AlertDialog.Title.Props`: `AlertDialogTitleProps`
- `AlertDialog.Title.State`: `AlertDialogTitleState`
- `AlertDialog.Trigger.Props`: `AlertDialogTriggerProps`
- `AlertDialog.Trigger.State`: `AlertDialogTriggerState`
- `AlertDialog.Viewport.State`: `AlertDialogViewportState`
- `AlertDialog.Viewport.Props`: `AlertDialogViewportProps`
#### createHandle
[//]: # '@exclude-table-of-contents'
##### Handle
### Autocomplete
A high-quality, unstyled React autocomplete component that renders an input with a list of filtered options.
#### Demo
##### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
'use client';
import { Autocomplete } from '@base-ui/react/autocomplete';
export default function ExampleAutocomplete() {
return (
Search tags
No tags found.
{(tag: Tag) => (
{tag.value}
)}
);
}
interface Tag {
id: string;
value: string;
}
const tags: Tag[] = [
{ id: 't1', value: 'feature' },
{ id: 't2', value: 'fix' },
{ id: 't3', value: 'bug' },
{ id: 't4', value: 'docs' },
{ id: 't5', value: 'internal' },
{ id: 't6', value: 'mobile' },
{ id: 'c-accordion', value: 'component: accordion' },
{ id: 'c-alert-dialog', value: 'component: alert dialog' },
{ id: 'c-autocomplete', value: 'component: autocomplete' },
{ id: 'c-avatar', value: 'component: avatar' },
{ id: 'c-checkbox', value: 'component: checkbox' },
{ id: 'c-checkbox-group', value: 'component: checkbox group' },
{ id: 'c-collapsible', value: 'component: collapsible' },
{ id: 'c-combobox', value: 'component: combobox' },
{ id: 'c-context-menu', value: 'component: context menu' },
{ id: 'c-dialog', value: 'component: dialog' },
{ id: 'c-field', value: 'component: field' },
{ id: 'c-fieldset', value: 'component: fieldset' },
{ id: 'c-filterable-menu', value: 'component: filterable menu' },
{ id: 'c-form', value: 'component: form' },
{ id: 'c-input', value: 'component: input' },
{ id: 'c-menu', value: 'component: menu' },
{ id: 'c-menubar', value: 'component: menubar' },
{ id: 'c-meter', value: 'component: meter' },
{ id: 'c-navigation-menu', value: 'component: navigation menu' },
{ id: 'c-number-field', value: 'component: number field' },
{ id: 'c-popover', value: 'component: popover' },
{ id: 'c-preview-card', value: 'component: preview card' },
{ id: 'c-progress', value: 'component: progress' },
{ id: 'c-radio', value: 'component: radio' },
{ id: 'c-scroll-area', value: 'component: scroll area' },
{ id: 'c-select', value: 'component: select' },
{ id: 'c-separator', value: 'component: separator' },
{ id: 'c-slider', value: 'component: slider' },
{ id: 'c-switch', value: 'component: switch' },
{ id: 'c-tabs', value: 'component: tabs' },
{ id: 'c-toast', value: 'component: toast' },
{ id: 'c-toggle', value: 'component: toggle' },
{ id: 'c-toggle-group', value: 'component: toggle group' },
{ id: 'c-toolbar', value: 'component: toolbar' },
{ id: 'c-tooltip', value: 'component: tooltip' },
];
```
##### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Input {
box-sizing: border-box;
padding: 0 0.5rem;
margin: 0;
border-radius: 0;
border: 1px solid oklch(14.5% 0 0deg);
width: 16rem;
height: 2rem;
font-family: inherit;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 400;
background-color: white;
color: oklch(14.5% 0 0deg);
outline: none;
@media (any-pointer: coarse) {
font-size: 1rem;
line-height: 1.5rem;
}
@media (prefers-color-scheme: dark) {
border: 1px solid white;
background-color: oklch(14.5% 0 0deg);
color: white;
}
&::placeholder {
color: oklch(55.6% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
&:focus {
outline: 2px solid oklch(14.5% 0 0deg);
outline-offset: -1px;
@media (prefers-color-scheme: dark) {
outline-color: white;
}
}
}
.Label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 700;
color: oklch(14.5% 0 0deg);
@media (prefers-color-scheme: dark) {
color: white;
}
}
.Positioner {
outline: 0;
}
.Popup {
box-sizing: border-box;
background-color: white;
color: oklch(14.5% 0 0deg);
width: var(--anchor-width);
max-width: var(--available-width);
border: 1px solid oklch(14.5% 0 0deg);
box-shadow: 0.25rem 0.25rem 0 rgb(0 0 0 / 12%);
@media (prefers-color-scheme: dark) {
background-color: oklch(14.5% 0 0deg);
color: white;
border: 1px solid white;
box-shadow: none;
}
}
.List {
box-sizing: border-box;
overflow-y: auto;
overscroll-behavior: contain;
padding-block: 0.25rem;
scroll-padding-block: 0.25rem;
outline: 0;
max-height: min(22.5rem, var(--available-height));
&[data-empty] {
padding: 0;
}
}
.Item {
box-sizing: border-box;
outline: 0;
cursor: default;
-webkit-user-select: none;
user-select: none;
padding-block: 0.5rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
display: flex;
font-size: 0.875rem;
line-height: 1rem;
&[data-highlighted] {
z-index: 0;
position: relative;
color: white;
@media (prefers-color-scheme: dark) {
color: oklch(14.5% 0 0deg);
}
}
&[data-highlighted]::before {
content: '';
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0;
background-color: oklch(14.5% 0 0deg);
@media (prefers-color-scheme: dark) {
background-color: white;
}
}
}
.Separator {
margin: 0.375rem 1rem;
height: 1px;
background-color: oklch(97% 0 0deg);
@media (prefers-color-scheme: dark) {
background-color: oklch(26.9% 0 0deg);
}
}
.Empty {
box-sizing: border-box;
padding: 1rem 1rem 1rem 0.5rem;
font-size: 0.875rem;
line-height: 1rem;
color: oklch(55.6% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
```
```tsx
/* index.tsx */
'use client';
import { Autocomplete } from '@base-ui/react/autocomplete';
import styles from './index.module.css';
export default function ExampleAutocomplete() {
return (
Search tags
No tags found.
{(tag: Tag) => (
{tag.value}
)}
);
}
interface Tag {
id: string;
value: string;
}
const tags: Tag[] = [
{ id: 't1', value: 'feature' },
{ id: 't2', value: 'fix' },
{ id: 't3', value: 'bug' },
{ id: 't4', value: 'docs' },
{ id: 't5', value: 'internal' },
{ id: 't6', value: 'mobile' },
{ id: 'c-accordion', value: 'component: accordion' },
{ id: 'c-alert-dialog', value: 'component: alert dialog' },
{ id: 'c-autocomplete', value: 'component: autocomplete' },
{ id: 'c-avatar', value: 'component: avatar' },
{ id: 'c-checkbox', value: 'component: checkbox' },
{ id: 'c-checkbox-group', value: 'component: checkbox group' },
{ id: 'c-collapsible', value: 'component: collapsible' },
{ id: 'c-combobox', value: 'component: combobox' },
{ id: 'c-context-menu', value: 'component: context menu' },
{ id: 'c-dialog', value: 'component: dialog' },
{ id: 'c-field', value: 'component: field' },
{ id: 'c-fieldset', value: 'component: fieldset' },
{ id: 'c-filterable-menu', value: 'component: filterable menu' },
{ id: 'c-form', value: 'component: form' },
{ id: 'c-input', value: 'component: input' },
{ id: 'c-menu', value: 'component: menu' },
{ id: 'c-menubar', value: 'component: menubar' },
{ id: 'c-meter', value: 'component: meter' },
{ id: 'c-navigation-menu', value: 'component: navigation menu' },
{ id: 'c-number-field', value: 'component: number field' },
{ id: 'c-popover', value: 'component: popover' },
{ id: 'c-preview-card', value: 'component: preview card' },
{ id: 'c-progress', value: 'component: progress' },
{ id: 'c-radio', value: 'component: radio' },
{ id: 'c-scroll-area', value: 'component: scroll area' },
{ id: 'c-select', value: 'component: select' },
{ id: 'c-separator', value: 'component: separator' },
{ id: 'c-slider', value: 'component: slider' },
{ id: 'c-switch', value: 'component: switch' },
{ id: 'c-tabs', value: 'component: tabs' },
{ id: 'c-toast', value: 'component: toast' },
{ id: 'c-toggle', value: 'component: toggle' },
{ id: 'c-toggle-group', value: 'component: toggle group' },
{ id: 'c-toolbar', value: 'component: toolbar' },
{ id: 'c-tooltip', value: 'component: tooltip' },
];
```
#### Usage guidelines
- **Avoid when selection state is needed**: Use [Combobox](#combobox) instead of Autocomplete if the selection should be remembered and the input value cannot be custom. Unlike Combobox, Autocomplete's input can contain free-form text, as its suggestions only _optionally_ autocomplete the text.
- **Can be used for filterable command pickers**: The input can be used as a filter for command items that perform an action when clicked when rendered inside the popup.
- **Form controls must have an accessible name**: It can be created using a `` element or the `Field` component. See the [forms guide](#forms).
#### Anatomy
Import the components and place them together:
```jsx title="Anatomy"
import { Autocomplete } from '@base-ui/react/autocomplete';
;
```
#### TypeScript inference
Autocomplete infers the item type from the `items` prop passed to ``.
If using `itemToStringValue`, the value prop on the `` must match the type of an item in the `items` array.
#### Examples
##### Async search
Load items asynchronously while typing and render custom status content.
#### Demo
##### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
'use client';
import * as React from 'react';
import { Autocomplete } from '@base-ui/react/autocomplete';
export default function ExampleAsyncAutocomplete() {
const [searchValue, setSearchValue] = React.useState('');
const [searchResults, setSearchResults] = React.useState([]);
const [error, setError] = React.useState(null);
const [isPending, startTransition] = React.useTransition();
const { contains } = Autocomplete.useFilter();
const abortControllerRef = React.useRef(null);
function getStatus(): React.ReactNode | null {
if (isPending) {
return (
Searching…
);
}
if (error) {
return error;
}
if (searchValue === '') {
return null;
}
if (searchResults.length === 0) {
return `Movie or year "${searchValue}" does not exist in the Top 100 IMDb movies`;
}
return `${searchResults.length} result${searchResults.length === 1 ? '' : 's'} found`;
}
const status = getStatus();
return (
{
setSearchValue(nextSearchValue);
const controller = new AbortController();
abortControllerRef.current?.abort();
abortControllerRef.current = controller;
if (nextSearchValue === '') {
setSearchResults([]);
setError(null);
return;
}
startTransition(async () => {
setError(null);
const result = await searchMovies(nextSearchValue, contains);
if (controller.signal.aborted) {
return;
}
startTransition(() => {
setSearchResults(result.movies);
setError(result.error);
});
});
}}
itemToStringValue={(item) => item.title}
filter={null}
>
Search movies by name or year
{status && (
{status}
)}
{(movie: Movie) => (
{movie.title}
{movie.year}
)}
);
}
async function searchMovies(
query: string,
filter: (item: string, query: string) => boolean,
): Promise<{ movies: Movie[]; error: string | null }> {
// Simulate network delay
await new Promise((resolve) => {
setTimeout(resolve, Math.random() * 500 + 100);
});
// Simulate occasional network errors (1% chance)
if (Math.random() < 0.01 || query === 'will_error') {
return {
movies: [],
error: 'Failed to fetch movies. Please try again.',
};
}
const movies = top100Movies.filter(
(movie) => filter(movie.title, query) || filter(movie.year.toString(), query),
);
return {
movies,
error: null,
};
}
interface Movie {
id: string;
title: string;
year: number;
}
const top100Movies: Movie[] = [
{ id: '1', title: 'The Shawshank Redemption', year: 1994 },
{ id: '2', title: 'The Godfather', year: 1972 },
{ id: '3', title: 'The Dark Knight', year: 2008 },
{ id: '4', title: 'The Godfather Part II', year: 1974 },
{ id: '5', title: '12 Angry Men', year: 1957 },
{ id: '6', title: 'The Lord of the Rings: The Return of the King', year: 2003 },
{ id: '7', title: "Schindler's List", year: 1993 },
{ id: '8', title: 'Pulp Fiction', year: 1994 },
{ id: '9', title: 'The Lord of the Rings: The Fellowship of the Ring', year: 2001 },
{ id: '10', title: 'The Good, the Bad and the Ugly', year: 1966 },
{ id: '11', title: 'Forrest Gump', year: 1994 },
{ id: '12', title: 'The Lord of the Rings: The Two Towers', year: 2002 },
{ id: '13', title: 'Fight Club', year: 1999 },
{ id: '14', title: 'Inception', year: 2010 },
{ id: '15', title: 'Star Wars: Episode V – The Empire Strikes Back', year: 1980 },
{ id: '16', title: 'The Matrix', year: 1999 },
{ id: '17', title: 'Goodfellas', year: 1990 },
{ id: '18', title: 'Interstellar', year: 2014 },
{ id: '19', title: "One Flew Over the Cuckoo's Nest", year: 1975 },
{ id: '20', title: 'Se7en', year: 1995 },
{ id: '21', title: "It's a Wonderful Life", year: 1946 },
{ id: '22', title: 'The Silence of the Lambs', year: 1991 },
{ id: '23', title: 'Seven Samurai', year: 1954 },
{ id: '24', title: 'Saving Private Ryan', year: 1998 },
{ id: '25', title: 'City of God', year: 2002 },
{ id: '26', title: 'Life Is Beautiful', year: 1997 },
{ id: '27', title: 'The Green Mile', year: 1999 },
{ id: '28', title: 'Star Wars: Episode IV – A New Hope', year: 1977 },
{ id: '29', title: 'Terminator 2: Judgment Day', year: 1991 },
{ id: '30', title: 'Back to the Future', year: 1985 },
{ id: '31', title: 'Spirited Away', year: 2001 },
{ id: '32', title: 'The Pianist', year: 2002 },
{ id: '33', title: 'Psycho', year: 1960 },
{ id: '34', title: 'Parasite', year: 2019 },
{ id: '35', title: 'Gladiator', year: 2000 },
{ id: '36', title: 'Léon: The Professional', year: 1994 },
{ id: '37', title: 'American History X', year: 1998 },
{ id: '38', title: 'The Departed', year: 2006 },
{ id: '39', title: 'Whiplash', year: 2014 },
{ id: '40', title: 'The Prestige', year: 2006 },
{ id: '41', title: 'Grave of the Fireflies', year: 1988 },
{ id: '42', title: 'The Usual Suspects', year: 1995 },
{ id: '43', title: 'Casablanca', year: 1942 },
{ id: '44', title: 'Harakiri', year: 1962 },
{ id: '45', title: 'The Lion King', year: 1994 },
{ id: '46', title: 'The Intouchables', year: 2011 },
{ id: '47', title: 'Modern Times', year: 1936 },
{ id: '48', title: 'The Lives of Others', year: 2006 },
{ id: '49', title: 'Once Upon a Time in the West', year: 1968 },
{ id: '50', title: 'Rear Window', year: 1954 },
{ id: '51', title: 'Alien', year: 1979 },
{ id: '52', title: 'City Lights', year: 1931 },
{ id: '53', title: 'The Shining', year: 1980 },
{ id: '54', title: 'Cinema Paradiso', year: 1988 },
{ id: '55', title: 'Avengers: Infinity War', year: 2018 },
{ id: '56', title: 'Paths of Glory', year: 1957 },
{ id: '57', title: 'Django Unchained', year: 2012 },
{ id: '58', title: 'WALL·E', year: 2008 },
{ id: '59', title: 'Sunset Boulevard', year: 1950 },
{ id: '60', title: 'The Great Dictator', year: 1940 },
{ id: '61', title: 'The Dark Knight Rises', year: 2012 },
{ id: '62', title: 'Princess Mononoke', year: 1997 },
{ id: '63', title: 'Witness for the Prosecution', year: 1957 },
{ id: '64', title: 'Oldboy', year: 2003 },
{ id: '65', title: 'Aliens', year: 1986 },
{ id: '66', title: 'Once Upon a Time in America', year: 1984 },
{ id: '67', title: 'Coco', year: 2017 },
{ id: '68', title: 'Your Name.', year: 2016 },
{ id: '69', title: 'American Beauty', year: 1999 },
{ id: '70', title: 'Braveheart', year: 1995 },
{ id: '71', title: 'Das Boot', year: 1981 },
{ id: '72', title: '3 Idiots', year: 2009 },
{ id: '73', title: 'Toy Story', year: 1995 },
{ id: '74', title: 'Inglourious Basterds', year: 2009 },
{ id: '75', title: 'High and Low', year: 1963 },
{ id: '76', title: 'Amadeus', year: 1984 },
{ id: '77', title: 'Good Will Hunting', year: 1997 },
{ id: '78', title: 'Star Wars: Episode VI – Return of the Jedi', year: 1983 },
{ id: '79', title: 'The Hunt', year: 2012 },
{ id: '80', title: 'Capharnaüm', year: 2018 },
{ id: '81', title: 'Reservoir Dogs', year: 1992 },
{ id: '82', title: 'Eternal Sunshine of the Spotless Mind', year: 2004 },
{ id: '83', title: 'Requiem for a Dream', year: 2000 },
{ id: '84', title: 'Come and See', year: 1985 },
{ id: '85', title: 'Ikiru', year: 1952 },
{ id: '86', title: 'Vertigo', year: 1958 },
{ id: '87', title: 'Lawrence of Arabia', year: 1962 },
{ id: '88', title: 'Citizen Kane', year: 1941 },
{ id: '89', title: 'Memento', year: 2000 },
{ id: '90', title: 'North by Northwest', year: 1959 },
{ id: '91', title: 'Star Wars: Episode III – Revenge of the Sith', year: 2005 },
{ id: '92', title: '2001: A Space Odyssey', year: 1968 },
{ id: '93', title: 'Amélie', year: 2001 },
{ id: '94', title: "Singin' in the Rain", year: 1952 },
{ id: '95', title: 'Apocalypse Now', year: 1979 },
{ id: '96', title: 'Taxi Driver', year: 1976 },
{ id: '97', title: 'Downfall', year: 2004 },
{ id: '98', title: 'The Wolf of Wall Street', year: 2013 },
{ id: '99', title: 'A Clockwork Orange', year: 1971 },
{ id: '100', title: 'Double Indemnity', year: 1944 },
];
```
##### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Input {
box-sizing: border-box;
padding: 0 0.5rem;
margin: 0;
border-radius: 0;
border: 1px solid oklch(14.5% 0 0deg);
width: 16rem;
height: 2rem;
font-family: inherit;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 400;
background-color: white;
color: oklch(14.5% 0 0deg);
outline: none;
@media (any-pointer: coarse) {
font-size: 1rem;
line-height: 1.5rem;
}
@media (prefers-color-scheme: dark) {
border: 1px solid white;
background-color: oklch(14.5% 0 0deg);
color: white;
}
&::placeholder {
color: oklch(55.6% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
&:focus {
outline: 2px solid oklch(14.5% 0 0deg);
outline-offset: -1px;
@media (prefers-color-scheme: dark) {
outline-color: white;
}
}
}
.Label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 700;
color: oklch(14.5% 0 0deg);
@media (prefers-color-scheme: dark) {
color: white;
}
}
.Positioner {
outline: 0;
}
.Popup {
box-sizing: border-box;
background-color: white;
color: oklch(14.5% 0 0deg);
width: var(--anchor-width);
max-width: var(--available-width);
border: 1px solid oklch(14.5% 0 0deg);
box-shadow: 0.25rem 0.25rem 0 rgb(0 0 0 / 12%);
@media (prefers-color-scheme: dark) {
background-color: oklch(14.5% 0 0deg);
color: white;
border: 1px solid white;
box-shadow: none;
}
}
.Viewport {
box-sizing: border-box;
max-height: min(var(--available-height), 22.5rem);
padding-block: 0.25rem;
overflow-y: auto;
overscroll-behavior: contain;
scroll-padding-block: 0.25rem;
}
.Item {
box-sizing: border-box;
outline: 0;
cursor: default;
-webkit-user-select: none;
user-select: none;
padding-block: 0.5rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
display: flex;
font-size: 0.875rem;
line-height: 1rem;
&[data-highlighted] {
z-index: 0;
position: relative;
color: white;
@media (prefers-color-scheme: dark) {
color: oklch(14.5% 0 0deg);
}
}
&[data-highlighted]::before {
content: '';
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0;
background-color: oklch(14.5% 0 0deg);
@media (prefers-color-scheme: dark) {
background-color: white;
}
}
}
.MovieItem {
display: flex;
flex-direction: column;
gap: 0.25rem;
width: 100%;
}
.MovieName {
font-weight: 700;
line-height: 1.25rem;
}
.MovieYear {
font-size: 0.875rem;
line-height: 1rem;
color: oklch(55.6% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
.Item[data-highlighted] & {
color: oklch(87% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(55.6% 0 0deg);
}
}
}
.Status {
display: flex;
align-items: center;
gap: 0.5rem;
padding-block: 0.25rem;
padding-left: 0.5rem;
padding-right: 2rem;
font-size: 0.875rem;
color: oklch(55.6% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
.Spinner {
box-sizing: border-box;
width: 0.75rem;
height: 0.75rem;
border-radius: 50%;
border: 1px solid currentColor;
border-right-color: transparent;
animation: autocompleteSpinner 0.75s linear infinite;
}
@keyframes autocompleteSpinner {
100% {
transform: rotate(360deg);
}
}
```
```tsx
/* index.tsx */
'use client';
import * as React from 'react';
import { Autocomplete } from '@base-ui/react/autocomplete';
import styles from './index.module.css';
export default function ExampleAsyncAutocomplete() {
const [searchValue, setSearchValue] = React.useState('');
const [searchResults, setSearchResults] = React.useState([]);
const [error, setError] = React.useState(null);
const [isPending, startTransition] = React.useTransition();
const { contains } = Autocomplete.useFilter();
const abortControllerRef = React.useRef(null);
function getStatus(): React.ReactNode | null {
if (isPending) {
return (
Searching…
);
}
if (error) {
return error;
}
if (searchValue === '') {
return null;
}
if (searchResults.length === 0) {
return `Movie or year "${searchValue}" does not exist in the Top 100 IMDb movies`;
}
return `${searchResults.length} result${searchResults.length === 1 ? '' : 's'} found`;
}
const status = getStatus();
return (
{
setSearchValue(nextSearchValue);
const controller = new AbortController();
abortControllerRef.current?.abort();
abortControllerRef.current = controller;
if (nextSearchValue === '') {
setSearchResults([]);
setError(null);
return;
}
startTransition(async () => {
setError(null);
const result = await searchMovies(nextSearchValue, contains);
if (controller.signal.aborted) {
return;
}
startTransition(() => {
setSearchResults(result.movies);
setError(result.error);
});
});
}}
itemToStringValue={(item) => item.title}
filter={null}
>
Search movies by name or year
{status && {status}
}
{(movie: Movie) => (
{movie.title}
{movie.year}
)}
);
}
async function searchMovies(
query: string,
filter: (item: string, query: string) => boolean,
): Promise<{ movies: Movie[]; error: string | null }> {
// Simulate network delay
await new Promise((resolve) => {
setTimeout(resolve, Math.random() * 500 + 100);
});
// Simulate occasional network errors (1% chance)
if (Math.random() < 0.01 || query === 'will_error') {
return {
movies: [],
error: 'Failed to fetch movies. Please try again.',
};
}
const movies = top100Movies.filter(
(movie) => filter(movie.title, query) || filter(movie.year.toString(), query),
);
return {
movies,
error: null,
};
}
interface Movie {
id: string;
title: string;
year: number;
}
const top100Movies: Movie[] = [
{ id: '1', title: 'The Shawshank Redemption', year: 1994 },
{ id: '2', title: 'The Godfather', year: 1972 },
{ id: '3', title: 'The Dark Knight', year: 2008 },
{ id: '4', title: 'The Godfather Part II', year: 1974 },
{ id: '5', title: '12 Angry Men', year: 1957 },
{ id: '6', title: 'The Lord of the Rings: The Return of the King', year: 2003 },
{ id: '7', title: "Schindler's List", year: 1993 },
{ id: '8', title: 'Pulp Fiction', year: 1994 },
{ id: '9', title: 'The Lord of the Rings: The Fellowship of the Ring', year: 2001 },
{ id: '10', title: 'The Good, the Bad and the Ugly', year: 1966 },
{ id: '11', title: 'Forrest Gump', year: 1994 },
{ id: '12', title: 'The Lord of the Rings: The Two Towers', year: 2002 },
{ id: '13', title: 'Fight Club', year: 1999 },
{ id: '14', title: 'Inception', year: 2010 },
{ id: '15', title: 'Star Wars: Episode V – The Empire Strikes Back', year: 1980 },
{ id: '16', title: 'The Matrix', year: 1999 },
{ id: '17', title: 'Goodfellas', year: 1990 },
{ id: '18', title: 'Interstellar', year: 2014 },
{ id: '19', title: "One Flew Over the Cuckoo's Nest", year: 1975 },
{ id: '20', title: 'Se7en', year: 1995 },
{ id: '21', title: "It's a Wonderful Life", year: 1946 },
{ id: '22', title: 'The Silence of the Lambs', year: 1991 },
{ id: '23', title: 'Seven Samurai', year: 1954 },
{ id: '24', title: 'Saving Private Ryan', year: 1998 },
{ id: '25', title: 'City of God', year: 2002 },
{ id: '26', title: 'Life Is Beautiful', year: 1997 },
{ id: '27', title: 'The Green Mile', year: 1999 },
{ id: '28', title: 'Star Wars: Episode IV – A New Hope', year: 1977 },
{ id: '29', title: 'Terminator 2: Judgment Day', year: 1991 },
{ id: '30', title: 'Back to the Future', year: 1985 },
{ id: '31', title: 'Spirited Away', year: 2001 },
{ id: '32', title: 'The Pianist', year: 2002 },
{ id: '33', title: 'Psycho', year: 1960 },
{ id: '34', title: 'Parasite', year: 2019 },
{ id: '35', title: 'Gladiator', year: 2000 },
{ id: '36', title: 'Léon: The Professional', year: 1994 },
{ id: '37', title: 'American History X', year: 1998 },
{ id: '38', title: 'The Departed', year: 2006 },
{ id: '39', title: 'Whiplash', year: 2014 },
{ id: '40', title: 'The Prestige', year: 2006 },
{ id: '41', title: 'Grave of the Fireflies', year: 1988 },
{ id: '42', title: 'The Usual Suspects', year: 1995 },
{ id: '43', title: 'Casablanca', year: 1942 },
{ id: '44', title: 'Harakiri', year: 1962 },
{ id: '45', title: 'The Lion King', year: 1994 },
{ id: '46', title: 'The Intouchables', year: 2011 },
{ id: '47', title: 'Modern Times', year: 1936 },
{ id: '48', title: 'The Lives of Others', year: 2006 },
{ id: '49', title: 'Once Upon a Time in the West', year: 1968 },
{ id: '50', title: 'Rear Window', year: 1954 },
{ id: '51', title: 'Alien', year: 1979 },
{ id: '52', title: 'City Lights', year: 1931 },
{ id: '53', title: 'The Shining', year: 1980 },
{ id: '54', title: 'Cinema Paradiso', year: 1988 },
{ id: '55', title: 'Avengers: Infinity War', year: 2018 },
{ id: '56', title: 'Paths of Glory', year: 1957 },
{ id: '57', title: 'Django Unchained', year: 2012 },
{ id: '58', title: 'WALL·E', year: 2008 },
{ id: '59', title: 'Sunset Boulevard', year: 1950 },
{ id: '60', title: 'The Great Dictator', year: 1940 },
{ id: '61', title: 'The Dark Knight Rises', year: 2012 },
{ id: '62', title: 'Princess Mononoke', year: 1997 },
{ id: '63', title: 'Witness for the Prosecution', year: 1957 },
{ id: '64', title: 'Oldboy', year: 2003 },
{ id: '65', title: 'Aliens', year: 1986 },
{ id: '66', title: 'Once Upon a Time in America', year: 1984 },
{ id: '67', title: 'Coco', year: 2017 },
{ id: '68', title: 'Your Name.', year: 2016 },
{ id: '69', title: 'American Beauty', year: 1999 },
{ id: '70', title: 'Braveheart', year: 1995 },
{ id: '71', title: 'Das Boot', year: 1981 },
{ id: '72', title: '3 Idiots', year: 2009 },
{ id: '73', title: 'Toy Story', year: 1995 },
{ id: '74', title: 'Inglourious Basterds', year: 2009 },
{ id: '75', title: 'High and Low', year: 1963 },
{ id: '76', title: 'Amadeus', year: 1984 },
{ id: '77', title: 'Good Will Hunting', year: 1997 },
{ id: '78', title: 'Star Wars: Episode VI – Return of the Jedi', year: 1983 },
{ id: '79', title: 'The Hunt', year: 2012 },
{ id: '80', title: 'Capharnaüm', year: 2018 },
{ id: '81', title: 'Reservoir Dogs', year: 1992 },
{ id: '82', title: 'Eternal Sunshine of the Spotless Mind', year: 2004 },
{ id: '83', title: 'Requiem for a Dream', year: 2000 },
{ id: '84', title: 'Come and See', year: 1985 },
{ id: '85', title: 'Ikiru', year: 1952 },
{ id: '86', title: 'Vertigo', year: 1958 },
{ id: '87', title: 'Lawrence of Arabia', year: 1962 },
{ id: '88', title: 'Citizen Kane', year: 1941 },
{ id: '89', title: 'Memento', year: 2000 },
{ id: '90', title: 'North by Northwest', year: 1959 },
{ id: '91', title: 'Star Wars: Episode III – Revenge of the Sith', year: 2005 },
{ id: '92', title: '2001: A Space Odyssey', year: 1968 },
{ id: '93', title: 'Amélie', year: 2001 },
{ id: '94', title: "Singin' in the Rain", year: 1952 },
{ id: '95', title: 'Apocalypse Now', year: 1979 },
{ id: '96', title: 'Taxi Driver', year: 1976 },
{ id: '97', title: 'Downfall', year: 2004 },
{ id: '98', title: 'The Wolf of Wall Street', year: 2013 },
{ id: '99', title: 'A Clockwork Orange', year: 1971 },
{ id: '100', title: 'Double Indemnity', year: 1944 },
];
```
##### Inline autocomplete
Autofill the input with the highlighted item while navigating with arrow keys using the `mode` prop. Accepts `aria-autocomplete` values `list`, `both`, `inline`, or `none`.
#### Demo
##### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
'use client';
import { Autocomplete } from '@base-ui/react/autocomplete';
export default function ExampleAutocompleteInline() {
return (
Search tags
{(tag: Tag) => (
{tag.value}
)}
);
}
interface Tag {
id: string;
value: string;
}
const tags: Tag[] = [
{ id: 't1', value: 'feature' },
{ id: 't2', value: 'fix' },
{ id: 't3', value: 'bug' },
{ id: 't4', value: 'docs' },
{ id: 't5', value: 'internal' },
{ id: 't6', value: 'mobile' },
{ id: 'c-accordion', value: 'component: accordion' },
{ id: 'c-alert-dialog', value: 'component: alert dialog' },
{ id: 'c-autocomplete', value: 'component: autocomplete' },
{ id: 'c-avatar', value: 'component: avatar' },
{ id: 'c-checkbox', value: 'component: checkbox' },
{ id: 'c-checkbox-group', value: 'component: checkbox group' },
{ id: 'c-collapsible', value: 'component: collapsible' },
{ id: 'c-combobox', value: 'component: combobox' },
{ id: 'c-context-menu', value: 'component: context menu' },
{ id: 'c-dialog', value: 'component: dialog' },
{ id: 'c-field', value: 'component: field' },
{ id: 'c-fieldset', value: 'component: fieldset' },
{ id: 'c-filterable-menu', value: 'component: filterable menu' },
{ id: 'c-form', value: 'component: form' },
{ id: 'c-input', value: 'component: input' },
{ id: 'c-menu', value: 'component: menu' },
{ id: 'c-menubar', value: 'component: menubar' },
{ id: 'c-meter', value: 'component: meter' },
{ id: 'c-navigation-menu', value: 'component: navigation menu' },
{ id: 'c-number-field', value: 'component: number field' },
{ id: 'c-popover', value: 'component: popover' },
{ id: 'c-preview-card', value: 'component: preview card' },
{ id: 'c-progress', value: 'component: progress' },
{ id: 'c-radio', value: 'component: radio' },
{ id: 'c-scroll-area', value: 'component: scroll area' },
{ id: 'c-select', value: 'component: select' },
{ id: 'c-separator', value: 'component: separator' },
{ id: 'c-slider', value: 'component: slider' },
{ id: 'c-switch', value: 'component: switch' },
{ id: 'c-tabs', value: 'component: tabs' },
{ id: 'c-toast', value: 'component: toast' },
{ id: 'c-toggle', value: 'component: toggle' },
{ id: 'c-toggle-group', value: 'component: toggle group' },
{ id: 'c-toolbar', value: 'component: toolbar' },
{ id: 'c-tooltip', value: 'component: tooltip' },
];
```
##### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Container {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.Input {
box-sizing: border-box;
padding: 0 0.5rem;
margin: 0;
border-radius: 0;
border: 1px solid oklch(14.5% 0 0deg);
width: 16rem;
height: 2rem;
font-family: inherit;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 400;
background-color: white;
color: oklch(14.5% 0 0deg);
outline: none;
@media (any-pointer: coarse) {
font-size: 1rem;
line-height: 1.5rem;
}
@media (prefers-color-scheme: dark) {
border: 1px solid white;
background-color: oklch(14.5% 0 0deg);
color: white;
}
&::placeholder {
color: oklch(55.6% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
&:focus {
outline: 2px solid oklch(14.5% 0 0deg);
outline-offset: -1px;
@media (prefers-color-scheme: dark) {
outline-color: white;
}
}
}
.Label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 700;
color: oklch(14.5% 0 0deg);
@media (prefers-color-scheme: dark) {
color: white;
}
}
.Positioner {
outline: 0;
&[data-empty] {
display: none;
}
}
.Popup {
box-sizing: border-box;
background-color: white;
color: oklch(14.5% 0 0deg);
width: var(--anchor-width);
max-width: var(--available-width);
border: 1px solid oklch(14.5% 0 0deg);
box-shadow: 0.25rem 0.25rem 0 rgb(0 0 0 / 12%);
@media (prefers-color-scheme: dark) {
background-color: oklch(14.5% 0 0deg);
color: white;
border: 1px solid white;
box-shadow: none;
}
}
.List {
box-sizing: border-box;
overflow-y: auto;
overscroll-behavior: contain;
padding-block: 0.25rem;
scroll-padding-block: 0.25rem;
outline: 0;
max-height: min(22.5rem, var(--available-height));
&[data-empty] {
padding: 0;
}
}
.Item {
box-sizing: border-box;
outline: 0;
cursor: default;
-webkit-user-select: none;
user-select: none;
padding-block: 0.5rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
display: flex;
align-items: center;
font-size: 0.875rem;
line-height: 1rem;
&[data-highlighted] {
z-index: 0;
position: relative;
color: white;
@media (prefers-color-scheme: dark) {
color: oklch(14.5% 0 0deg);
}
}
&[data-highlighted]::before {
content: '';
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0;
background-color: oklch(14.5% 0 0deg);
@media (prefers-color-scheme: dark) {
background-color: white;
}
}
}
```
```tsx
/* index.tsx */
'use client';
import { Autocomplete } from '@base-ui/react/autocomplete';
import styles from './index.module.css';
export default function ExampleAutocompleteInline() {
return (
Search tags
{(tag: Tag) => (
{tag.value}
)}
);
}
interface Tag {
id: string;
value: string;
}
const tags: Tag[] = [
{ id: 't1', value: 'feature' },
{ id: 't2', value: 'fix' },
{ id: 't3', value: 'bug' },
{ id: 't4', value: 'docs' },
{ id: 't5', value: 'internal' },
{ id: 't6', value: 'mobile' },
{ id: 'c-accordion', value: 'component: accordion' },
{ id: 'c-alert-dialog', value: 'component: alert dialog' },
{ id: 'c-autocomplete', value: 'component: autocomplete' },
{ id: 'c-avatar', value: 'component: avatar' },
{ id: 'c-checkbox', value: 'component: checkbox' },
{ id: 'c-checkbox-group', value: 'component: checkbox group' },
{ id: 'c-collapsible', value: 'component: collapsible' },
{ id: 'c-combobox', value: 'component: combobox' },
{ id: 'c-context-menu', value: 'component: context menu' },
{ id: 'c-dialog', value: 'component: dialog' },
{ id: 'c-field', value: 'component: field' },
{ id: 'c-fieldset', value: 'component: fieldset' },
{ id: 'c-filterable-menu', value: 'component: filterable menu' },
{ id: 'c-form', value: 'component: form' },
{ id: 'c-input', value: 'component: input' },
{ id: 'c-menu', value: 'component: menu' },
{ id: 'c-menubar', value: 'component: menubar' },
{ id: 'c-meter', value: 'component: meter' },
{ id: 'c-navigation-menu', value: 'component: navigation menu' },
{ id: 'c-number-field', value: 'component: number field' },
{ id: 'c-popover', value: 'component: popover' },
{ id: 'c-preview-card', value: 'component: preview card' },
{ id: 'c-progress', value: 'component: progress' },
{ id: 'c-radio', value: 'component: radio' },
{ id: 'c-scroll-area', value: 'component: scroll area' },
{ id: 'c-select', value: 'component: select' },
{ id: 'c-separator', value: 'component: separator' },
{ id: 'c-slider', value: 'component: slider' },
{ id: 'c-switch', value: 'component: switch' },
{ id: 'c-tabs', value: 'component: tabs' },
{ id: 'c-toast', value: 'component: toast' },
{ id: 'c-toggle', value: 'component: toggle' },
{ id: 'c-toggle-group', value: 'component: toggle group' },
{ id: 'c-toolbar', value: 'component: toolbar' },
{ id: 'c-tooltip', value: 'component: tooltip' },
];
```
##### Grouped
Organize related options with `` and `` to add section headings inside the popup.
Groups are represented by an array of objects with an `items` property, which itself is an array of individual items for each group. An extra property, such as `value`, can be provided for the heading text when rendering the group label.
```tsx title="Example"
interface ProduceGroupItem {
value: string;
// @highlight
items: string[];
}
const groups: ProduceGroupItem[] = [
{
value: 'Fruits',
// @highlight
items: ['Apple', 'Banana', 'Orange'],
},
{
value: 'Vegetables',
// @highlight
items: ['Carrot', 'Lettuce', 'Spinach'],
},
];
```
#### Demo
##### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
'use client';
import { Autocomplete } from '@base-ui/react/autocomplete';
export default function ExampleGroupAutocomplete() {
return (
Select a tag
No tags found.
{(group: TagGroup) => (
{group.value}
{(tag: Tag) => (
{tag.label}
)}
)}
);
}
interface Tag {
id: string;
label: string;
group: 'Type' | 'Component';
}
interface TagGroup {
value: string;
items: Tag[];
}
const tagsData: Tag[] = [
{ id: 't1', label: 'feature', group: 'Type' },
{ id: 't2', label: 'fix', group: 'Type' },
{ id: 't3', label: 'bug', group: 'Type' },
{ id: 't4', label: 'docs', group: 'Type' },
{ id: 't5', label: 'internal', group: 'Type' },
{ id: 't6', label: 'mobile', group: 'Type' },
{ id: 'c-accordion', label: 'component: accordion', group: 'Component' },
{ id: 'c-alert-dialog', label: 'component: alert dialog', group: 'Component' },
{ id: 'c-autocomplete', label: 'component: autocomplete', group: 'Component' },
{ id: 'c-avatar', label: 'component: avatar', group: 'Component' },
{ id: 'c-checkbox', label: 'component: checkbox', group: 'Component' },
{ id: 'c-checkbox-group', label: 'component: checkbox group', group: 'Component' },
{ id: 'c-collapsible', label: 'component: collapsible', group: 'Component' },
{ id: 'c-combobox', label: 'component: combobox', group: 'Component' },
{ id: 'c-context-menu', label: 'component: context menu', group: 'Component' },
{ id: 'c-dialog', label: 'component: dialog', group: 'Component' },
{ id: 'c-field', label: 'component: field', group: 'Component' },
{ id: 'c-fieldset', label: 'component: fieldset', group: 'Component' },
{ id: 'c-filterable-menu', label: 'component: filterable menu', group: 'Component' },
{ id: 'c-form', label: 'component: form', group: 'Component' },
{ id: 'c-input', label: 'component: input', group: 'Component' },
{ id: 'c-menu', label: 'component: menu', group: 'Component' },
{ id: 'c-menubar', label: 'component: menubar', group: 'Component' },
{ id: 'c-meter', label: 'component: meter', group: 'Component' },
{ id: 'c-navigation-menu', label: 'component: navigation menu', group: 'Component' },
{ id: 'c-number-field', label: 'component: number field', group: 'Component' },
{ id: 'c-popover', label: 'component: popover', group: 'Component' },
{ id: 'c-preview-card', label: 'component: preview card', group: 'Component' },
{ id: 'c-progress', label: 'component: progress', group: 'Component' },
{ id: 'c-radio', label: 'component: radio', group: 'Component' },
{ id: 'c-scroll-area', label: 'component: scroll area', group: 'Component' },
{ id: 'c-select', label: 'component: select', group: 'Component' },
{ id: 'c-separator', label: 'component: separator', group: 'Component' },
{ id: 'c-slider', label: 'component: slider', group: 'Component' },
{ id: 'c-switch', label: 'component: switch', group: 'Component' },
{ id: 'c-tabs', label: 'component: tabs', group: 'Component' },
{ id: 'c-toast', label: 'component: toast', group: 'Component' },
{ id: 'c-toggle', label: 'component: toggle', group: 'Component' },
{ id: 'c-toggle-group', label: 'component: toggle group', group: 'Component' },
{ id: 'c-toolbar', label: 'component: toolbar', group: 'Component' },
{ id: 'c-tooltip', label: 'component: tooltip', group: 'Component' },
];
function groupTags(tags: Tag[]): TagGroup[] {
const groups: { [key: string]: Tag[] } = {};
tags.forEach((t) => {
(groups[t.group] ??= []).push(t);
});
const order = ['Type', 'Component'];
return order.map((value) => ({ value, items: groups[value] ?? [] }));
}
const groupedTags: TagGroup[] = groupTags(tagsData);
```
##### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Input {
box-sizing: border-box;
padding: 0 0.5rem;
margin: 0;
border-radius: 0;
border: 1px solid oklch(14.5% 0 0deg);
width: 16rem;
height: 2rem;
font-family: inherit;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 400;
background-color: white;
color: oklch(14.5% 0 0deg);
outline: none;
@media (any-pointer: coarse) {
font-size: 1rem;
line-height: 1.5rem;
}
@media (prefers-color-scheme: dark) {
border: 1px solid white;
background-color: oklch(14.5% 0 0deg);
color: white;
}
&::placeholder {
color: oklch(55.6% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
&:focus {
outline: 2px solid oklch(14.5% 0 0deg);
outline-offset: -1px;
@media (prefers-color-scheme: dark) {
outline-color: white;
}
}
}
.Label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 700;
color: oklch(14.5% 0 0deg);
@media (prefers-color-scheme: dark) {
color: white;
}
}
.Positioner {
outline: 0;
}
.Popup {
box-sizing: border-box;
background-color: white;
color: oklch(14.5% 0 0deg);
width: var(--anchor-width);
max-width: var(--available-width);
border: 1px solid oklch(14.5% 0 0deg);
box-shadow: 0.25rem 0.25rem 0 rgb(0 0 0 / 12%);
@media (prefers-color-scheme: dark) {
background-color: oklch(14.5% 0 0deg);
color: white;
border: 1px solid white;
box-shadow: none;
}
}
.List {
box-sizing: border-box;
overflow-y: auto;
padding-block: 0.25rem;
scroll-padding-top: 0.25rem;
scroll-padding-bottom: 0.25rem;
overscroll-behavior: contain;
max-height: min(22.5rem, var(--available-height));
outline: 0;
&[data-empty] {
padding: 0;
}
}
.Group {
display: block;
padding-bottom: 0.5rem;
&:last-child {
padding-bottom: 0;
}
}
.GroupLabel {
box-sizing: border-box;
-webkit-user-select: none;
user-select: none;
padding: 0.5rem;
font-size: 0.875rem;
line-height: 1rem;
color: oklch(55.6% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
.Item {
box-sizing: border-box;
outline: 0;
cursor: default;
-webkit-user-select: none;
user-select: none;
padding: 0.5rem;
display: flex;
font-size: 0.875rem;
line-height: 1rem;
&[data-highlighted] {
z-index: 0;
position: relative;
color: white;
@media (prefers-color-scheme: dark) {
color: oklch(14.5% 0 0deg);
}
}
&[data-highlighted]::before {
content: '';
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0;
background-color: oklch(14.5% 0 0deg);
@media (prefers-color-scheme: dark) {
background-color: white;
}
}
}
.Separator {
margin: 0.375rem 1rem;
height: 1px;
background-color: oklch(97% 0 0deg);
@media (prefers-color-scheme: dark) {
background-color: oklch(26.9% 0 0deg);
}
}
.Empty {
box-sizing: border-box;
padding: 1rem 1rem 1rem 0.5rem;
font-size: 0.875rem;
line-height: 1rem;
color: oklch(55.6% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
```
```tsx
/* index.tsx */
'use client';
import { Autocomplete } from '@base-ui/react/autocomplete';
import styles from './index.module.css';
export default function ExampleGroupAutocomplete() {
return (
Select a tag
No tags found.
{(group: TagGroup) => (
{group.value}
{(tag: Tag) => (
{tag.label}
)}
)}
);
}
interface Tag {
id: string;
label: string;
group: 'Type' | 'Component';
}
interface TagGroup {
value: string;
items: Tag[];
}
const tagsData: Tag[] = [
{ id: 't1', label: 'feature', group: 'Type' },
{ id: 't2', label: 'fix', group: 'Type' },
{ id: 't3', label: 'bug', group: 'Type' },
{ id: 't4', label: 'docs', group: 'Type' },
{ id: 't5', label: 'internal', group: 'Type' },
{ id: 't6', label: 'mobile', group: 'Type' },
{ id: 'c-accordion', label: 'component: accordion', group: 'Component' },
{ id: 'c-alert-dialog', label: 'component: alert dialog', group: 'Component' },
{ id: 'c-autocomplete', label: 'component: autocomplete', group: 'Component' },
{ id: 'c-avatar', label: 'component: avatar', group: 'Component' },
{ id: 'c-checkbox', label: 'component: checkbox', group: 'Component' },
{ id: 'c-checkbox-group', label: 'component: checkbox group', group: 'Component' },
{ id: 'c-collapsible', label: 'component: collapsible', group: 'Component' },
{ id: 'c-combobox', label: 'component: combobox', group: 'Component' },
{ id: 'c-context-menu', label: 'component: context menu', group: 'Component' },
{ id: 'c-dialog', label: 'component: dialog', group: 'Component' },
{ id: 'c-field', label: 'component: field', group: 'Component' },
{ id: 'c-fieldset', label: 'component: fieldset', group: 'Component' },
{ id: 'c-filterable-menu', label: 'component: filterable menu', group: 'Component' },
{ id: 'c-form', label: 'component: form', group: 'Component' },
{ id: 'c-input', label: 'component: input', group: 'Component' },
{ id: 'c-menu', label: 'component: menu', group: 'Component' },
{ id: 'c-menubar', label: 'component: menubar', group: 'Component' },
{ id: 'c-meter', label: 'component: meter', group: 'Component' },
{ id: 'c-navigation-menu', label: 'component: navigation menu', group: 'Component' },
{ id: 'c-number-field', label: 'component: number field', group: 'Component' },
{ id: 'c-popover', label: 'component: popover', group: 'Component' },
{ id: 'c-preview-card', label: 'component: preview card', group: 'Component' },
{ id: 'c-progress', label: 'component: progress', group: 'Component' },
{ id: 'c-radio', label: 'component: radio', group: 'Component' },
{ id: 'c-scroll-area', label: 'component: scroll area', group: 'Component' },
{ id: 'c-select', label: 'component: select', group: 'Component' },
{ id: 'c-separator', label: 'component: separator', group: 'Component' },
{ id: 'c-slider', label: 'component: slider', group: 'Component' },
{ id: 'c-switch', label: 'component: switch', group: 'Component' },
{ id: 'c-tabs', label: 'component: tabs', group: 'Component' },
{ id: 'c-toast', label: 'component: toast', group: 'Component' },
{ id: 'c-toggle', label: 'component: toggle', group: 'Component' },
{ id: 'c-toggle-group', label: 'component: toggle group', group: 'Component' },
{ id: 'c-toolbar', label: 'component: toolbar', group: 'Component' },
{ id: 'c-tooltip', label: 'component: tooltip', group: 'Component' },
];
function groupTags(tags: Tag[]): TagGroup[] {
const groups: { [key: string]: Tag[] } = {};
tags.forEach((t) => {
(groups[t.group] ??= []).push(t);
});
const order = ['Type', 'Component'];
return order.map((value) => ({ value, items: groups[value] ?? [] }));
}
const groupedTags: TagGroup[] = groupTags(tagsData);
```
##### Fuzzy matching
Use fuzzy matching to find relevant results even when the query doesn't exactly match the item text.
#### Demo
##### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
'use client';
import * as React from 'react';
import { Autocomplete } from '@base-ui/react/autocomplete';
import { matchSorter } from 'match-sorter';
export default function ExampleFuzzyMatchingAutocomplete() {
return (
item.title}
>
Fuzzy search documentation
No results found for "{
}"
{(item: FuzzyItem) => (
{(value) => (
{highlightText(item.title, value)}
{highlightText(item.description, value)}
)}
)}
);
}
function highlightText(text: string, query: string): React.ReactNode {
const trimmed = query.trim();
if (!trimmed) {
return text;
}
const limited = trimmed.slice(0, 100);
const escaped = limited.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escaped})`, 'gi');
return text.split(regex).map((part, idx) =>
regex.test(part) ? (
{part}
) : (
part
),
);
}
function fuzzyFilter(item: FuzzyItem, query: string): boolean {
if (!query) {
return true;
}
const results = matchSorter([item], query, {
keys: [
'title',
'description',
'category',
{ key: 'title', threshold: matchSorter.rankings.CONTAINS },
{ key: 'description', threshold: matchSorter.rankings.WORD_STARTS_WITH },
],
});
return results.length > 0;
}
interface FuzzyItem {
title: string;
description: string;
category: string;
}
const fuzzyItems: FuzzyItem[] = [
{
title: 'React Hooks Guide',
description: 'Learn how to use React Hooks like useState, useEffect, and custom hooks',
category: 'React',
},
{
title: 'JavaScript Array Methods',
description: 'Master array methods like map, filter, reduce, and forEach in JavaScript',
category: 'JavaScript',
},
{
title: 'CSS Flexbox Layout',
description: 'Complete guide to CSS Flexbox for responsive web design',
category: 'CSS',
},
{
title: 'TypeScript Interfaces',
description: 'Understanding TypeScript interfaces and type definitions',
category: 'TypeScript',
},
{
title: 'React Performance Optimization',
description: 'Tips and techniques for optimizing React application performance',
category: 'React',
},
{
title: 'HTML Semantic Elements',
description: 'Using semantic HTML elements for better accessibility and SEO',
category: 'HTML',
},
{
title: 'Node.js Express Server',
description: 'Building RESTful APIs with Node.js and Express framework',
category: 'Node.js',
},
{
title: 'Vue Composition API',
description: 'Modern Vue.js development using the Composition API',
category: 'Vue.js',
},
{
title: 'Angular Components',
description: 'Creating reusable Angular components with TypeScript',
category: 'Angular',
},
{
title: 'Python Django Framework',
description: 'Web development with Python Django framework',
category: 'Python',
},
{
title: 'CSS Grid Layout',
description: 'Advanced CSS Grid techniques for complex layouts',
category: 'CSS',
},
{
title: 'React Testing Library',
description: 'Testing React components with React Testing Library',
category: 'React',
},
{
title: 'MongoDB Queries',
description: 'Advanced MongoDB queries and aggregation pipelines',
category: 'Database',
},
{
title: 'Webpack Configuration',
description: 'Optimizing webpack configuration for production builds',
category: 'Build Tools',
},
{
title: 'SASS/SCSS Guide',
description: 'Writing maintainable CSS with SASS and SCSS',
category: 'CSS',
},
];
```
##### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 700;
color: oklch(14.5% 0 0deg);
@media (prefers-color-scheme: dark) {
color: white;
}
}
.Input {
box-sizing: border-box;
padding: 0 0.5rem;
margin: 0;
border-radius: 0;
border: 1px solid oklch(14.5% 0 0deg);
width: 16rem;
height: 2rem;
font-family: inherit;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 400;
background-color: white;
color: oklch(14.5% 0 0deg);
outline: none;
@media (any-pointer: coarse) {
font-size: 1rem;
line-height: 1.5rem;
}
@media (prefers-color-scheme: dark) {
border: 1px solid white;
background-color: oklch(14.5% 0 0deg);
color: white;
}
&::placeholder {
color: oklch(55.6% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
&:focus {
outline: 2px solid oklch(14.5% 0 0deg);
outline-offset: -1px;
@media (prefers-color-scheme: dark) {
outline-color: white;
}
}
}
.Positioner {
outline: 0;
}
.Popup {
box-sizing: border-box;
background-color: white;
color: oklch(14.5% 0 0deg);
width: var(--anchor-width);
max-width: var(--available-width);
border: 1px solid oklch(14.5% 0 0deg);
box-shadow: 0.25rem 0.25rem 0 rgb(0 0 0 / 12%);
@media (prefers-color-scheme: dark) {
background-color: oklch(14.5% 0 0deg);
color: white;
border: 1px solid white;
box-shadow: none;
}
}
.List {
box-sizing: border-box;
display: flex;
flex-direction: column;
padding-block: 0.25rem;
max-height: min(var(--available-height), 28rem);
overflow-y: auto;
overscroll-behavior: contain;
scroll-padding-block: 0.25rem;
&:empty {
padding: 0;
}
}
.Item {
box-sizing: border-box;
outline: 0;
cursor: default;
-webkit-user-select: none;
user-select: none;
padding: 0.75rem 0.5rem;
display: flex;
font-size: 0.875rem;
line-height: 1.5rem;
&[data-highlighted] {
z-index: 0;
position: relative;
}
&[data-highlighted]::before {
content: '';
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0;
background-color: oklch(97% 0 0deg);
@media (prefers-color-scheme: dark) {
background-color: oklch(26.9% 0 0deg);
}
}
}
.ItemContent {
display: flex;
flex-direction: column;
gap: 0.25rem;
width: 100%;
}
.ItemHeader {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.75rem;
}
.ItemTitle {
font-weight: 700;
line-height: 1.25rem;
flex: 1;
}
.ItemDescription {
font-size: 0.875rem;
color: oklch(55.6% 0 0deg);
line-height: 1.25rem;
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
.Empty {
box-sizing: border-box;
font-size: 0.875rem;
line-height: 1rem;
color: oklch(55.6% 0 0deg);
padding: 0.75rem 0.5rem;
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
.Item mark {
background-color: transparent;
color: oklch(42.4% 0.199 265.638deg);
font-weight: 700;
@media (prefers-color-scheme: dark) {
color: oklch(62.3% 0.214 259.815deg);
}
}
```
```tsx
/* index.tsx */
'use client';
import * as React from 'react';
import { Autocomplete } from '@base-ui/react/autocomplete';
import { matchSorter } from 'match-sorter';
import styles from './index.module.css';
export default function ExampleFuzzyMatchingAutocomplete() {
return (
item.title}
>
Fuzzy search documentation
No results found for "{
}"
{(item: FuzzyItem) => (
{(value) => (
{highlightText(item.title, value)}
{highlightText(item.description, value)}
)}
)}
);
}
function highlightText(text: string, query: string): React.ReactNode {
const trimmed = query.trim();
if (!trimmed) {
return text;
}
const limited = trimmed.slice(0, 100);
const escaped = limited.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escaped})`, 'gi');
return text
.split(regex)
.map((part, idx) => (regex.test(part) ? {part} : part));
}
function fuzzyFilter(item: FuzzyItem, query: string): boolean {
if (!query) {
return true;
}
const results = matchSorter([item], query, {
keys: [
'title',
'description',
'category',
{ key: 'title', threshold: matchSorter.rankings.CONTAINS },
{ key: 'description', threshold: matchSorter.rankings.WORD_STARTS_WITH },
],
});
return results.length > 0;
}
interface FuzzyItem {
title: string;
description: string;
category: string;
}
const fuzzyItems: FuzzyItem[] = [
{
title: 'React Hooks Guide',
description: 'Learn how to use React Hooks like useState, useEffect, and custom hooks',
category: 'React',
},
{
title: 'JavaScript Array Methods',
description: 'Master array methods like map, filter, reduce, and forEach in JavaScript',
category: 'JavaScript',
},
{
title: 'CSS Flexbox Layout',
description: 'Complete guide to CSS Flexbox for responsive web design',
category: 'CSS',
},
{
title: 'TypeScript Interfaces',
description: 'Understanding TypeScript interfaces and type definitions',
category: 'TypeScript',
},
{
title: 'React Performance Optimization',
description: 'Tips and techniques for optimizing React application performance',
category: 'React',
},
{
title: 'HTML Semantic Elements',
description: 'Using semantic HTML elements for better accessibility and SEO',
category: 'HTML',
},
{
title: 'Node.js Express Server',
description: 'Building RESTful APIs with Node.js and Express framework',
category: 'Node.js',
},
{
title: 'Vue Composition API',
description: 'Modern Vue.js development using the Composition API',
category: 'Vue.js',
},
{
title: 'Angular Components',
description: 'Creating reusable Angular components with TypeScript',
category: 'Angular',
},
{
title: 'Python Django Framework',
description: 'Web development with Python Django framework',
category: 'Python',
},
{
title: 'CSS Grid Layout',
description: 'Advanced CSS Grid techniques for complex layouts',
category: 'CSS',
},
{
title: 'React Testing Library',
description: 'Testing React components with React Testing Library',
category: 'React',
},
{
title: 'MongoDB Queries',
description: 'Advanced MongoDB queries and aggregation pipelines',
category: 'Database',
},
{
title: 'Webpack Configuration',
description: 'Optimizing webpack configuration for production builds',
category: 'Build Tools',
},
{
title: 'SASS/SCSS Guide',
description: 'Writing maintainable CSS with SASS and SCSS',
category: 'CSS',
},
];
```
##### Limit results
Limit the number of visible items using the `limit` prop and guide users to refine their query using ``.
#### Demo
##### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
'use client';
import * as React from 'react';
import { Autocomplete } from '@base-ui/react/autocomplete';
const limit = 8;
export default function ExampleAutocompleteLimit() {
const [value, setValue] = React.useState('');
const { contains } = Autocomplete.useFilter({ sensitivity: 'base' });
const totalMatches = React.useMemo(() => {
const trimmed = value.trim();
if (!trimmed) {
return tags.length;
}
return tags.filter((t) => contains(t.value, trimmed)).length;
}, [value, contains]);
const moreCount = Math.max(0, totalMatches - limit);
return (
Limit results to 8
No results found for "{value}"
{(tag: Tag) => (
{tag.value}
)}
{moreCount > 0 ? (
{`Hiding ${moreCount} results (type a more specific query to narrow results)`}
) : null}
);
}
interface Tag {
id: string;
value: string;
}
// Larger dataset to make the limit visible.
const tags: Tag[] = [
{ id: 't1', value: 'feature' },
{ id: 't2', value: 'fix' },
{ id: 't3', value: 'bug' },
{ id: 't4', value: 'docs' },
{ id: 't5', value: 'internal' },
{ id: 't6', value: 'mobile' },
{ id: 't7', value: 'frontend' },
{ id: 't8', value: 'backend' },
{ id: 't9', value: 'performance' },
{ id: 't10', value: 'accessibility' },
{ id: 't11', value: 'design' },
{ id: 't12', value: 'research' },
{ id: 't13', value: 'testing' },
{ id: 't14', value: 'infrastructure' },
{ id: 't15', value: 'documentation' },
{ id: 'c-accordion', value: 'component: accordion' },
{ id: 'c-alert-dialog', value: 'component: alert dialog' },
{ id: 'c-autocomplete', value: 'component: autocomplete' },
{ id: 'c-avatar', value: 'component: avatar' },
{ id: 'c-checkbox', value: 'component: checkbox' },
{ id: 'c-checkbox-group', value: 'component: checkbox group' },
{ id: 'c-collapsible', value: 'component: collapsible' },
{ id: 'c-combobox', value: 'component: combobox' },
{ id: 'c-context-menu', value: 'component: context menu' },
{ id: 'c-dialog', value: 'component: dialog' },
{ id: 'c-field', value: 'component: field' },
{ id: 'c-fieldset', value: 'component: fieldset' },
{ id: 'c-filterable-menu', value: 'component: filterable menu' },
{ id: 'c-form', value: 'component: form' },
{ id: 'c-input', value: 'component: input' },
{ id: 'c-menu', value: 'component: menu' },
{ id: 'c-menubar', value: 'component: menubar' },
{ id: 'c-meter', value: 'component: meter' },
{ id: 'c-navigation-menu', value: 'component: navigation menu' },
{ id: 'c-number-field', value: 'component: number field' },
{ id: 'c-popover', value: 'component: popover' },
{ id: 'c-preview-card', value: 'component: preview card' },
{ id: 'c-progress', value: 'component: progress' },
{ id: 'c-radio', value: 'component: radio' },
{ id: 'c-scroll-area', value: 'component: scroll area' },
{ id: 'c-select', value: 'component: select' },
{ id: 'c-separator', value: 'component: separator' },
{ id: 'c-slider', value: 'component: slider' },
{ id: 'c-switch', value: 'component: switch' },
{ id: 'c-tabs', value: 'component: tabs' },
{ id: 'c-toast', value: 'component: toast' },
{ id: 'c-toggle', value: 'component: toggle' },
{ id: 'c-toggle-group', value: 'component: toggle group' },
{ id: 'c-toolbar', value: 'component: toolbar' },
{ id: 'c-tooltip', value: 'component: tooltip' },
];
```
##### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Input {
box-sizing: border-box;
padding: 0 0.5rem;
margin: 0;
border-radius: 0;
border: 1px solid oklch(14.5% 0 0deg);
width: 16rem;
height: 2rem;
font-family: inherit;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 400;
background-color: white;
color: oklch(14.5% 0 0deg);
outline: none;
@media (any-pointer: coarse) {
font-size: 1rem;
line-height: 1.5rem;
}
@media (prefers-color-scheme: dark) {
border: 1px solid white;
background-color: oklch(14.5% 0 0deg);
color: white;
}
&::placeholder {
color: oklch(55.6% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
&:focus {
outline: 2px solid oklch(14.5% 0 0deg);
outline-offset: -1px;
@media (prefers-color-scheme: dark) {
outline-color: white;
}
}
}
.Label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 700;
color: oklch(14.5% 0 0deg);
@media (prefers-color-scheme: dark) {
color: white;
}
}
.Positioner {
outline: 0;
}
.Popup {
box-sizing: border-box;
padding-block: 0.25rem;
background-color: white;
color: oklch(14.5% 0 0deg);
width: var(--anchor-width);
max-height: min(var(--available-height), 22.5rem);
max-width: var(--available-width);
overflow-y: auto;
scroll-padding-block: 0.25rem;
overscroll-behavior: contain;
border: 1px solid oklch(14.5% 0 0deg);
box-shadow: 0.25rem 0.25rem 0 rgb(0 0 0 / 12%);
@media (prefers-color-scheme: dark) {
background-color: oklch(14.5% 0 0deg);
color: white;
border: 1px solid white;
box-shadow: none;
}
}
.Item {
box-sizing: border-box;
outline: 0;
cursor: default;
-webkit-user-select: none;
user-select: none;
padding-block: 0.5rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
display: flex;
font-size: 0.875rem;
line-height: 1rem;
&[data-highlighted] {
z-index: 0;
position: relative;
color: white;
@media (prefers-color-scheme: dark) {
color: oklch(14.5% 0 0deg);
}
}
&[data-highlighted]::before {
content: '';
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0;
background-color: oklch(14.5% 0 0deg);
@media (prefers-color-scheme: dark) {
background-color: white;
}
}
}
.Empty {
box-sizing: border-box;
padding: 0.5rem 1rem 0.5rem 0.5rem;
font-size: 0.875rem;
line-height: 1rem;
color: oklch(55.6% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
.Status {
box-sizing: border-box;
padding: 0.5rem 1rem 0.5rem 0.5rem;
font-size: 0.875rem;
line-height: 1.25rem;
color: oklch(55.6% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
```
```tsx
/* index.tsx */
'use client';
import * as React from 'react';
import { Autocomplete } from '@base-ui/react/autocomplete';
import styles from './index.module.css';
const limit = 8;
export default function ExampleAutocompleteLimit() {
const [value, setValue] = React.useState('');
const { contains } = Autocomplete.useFilter({ sensitivity: 'base' });
const totalMatches = React.useMemo(() => {
const trimmed = value.trim();
if (!trimmed) {
return tags.length;
}
return tags.filter((t) => contains(t.value, trimmed)).length;
}, [value, contains]);
const moreCount = Math.max(0, totalMatches - limit);
return (
Limit results to 8
No results found for "{value}"
{(tag: Tag) => (
{tag.value}
)}
{moreCount > 0 ? (
{`Hiding ${moreCount} results (type a more specific query to narrow results)`}
) : null}
);
}
interface Tag {
id: string;
value: string;
}
// Larger dataset to make the limit visible.
const tags: Tag[] = [
{ id: 't1', value: 'feature' },
{ id: 't2', value: 'fix' },
{ id: 't3', value: 'bug' },
{ id: 't4', value: 'docs' },
{ id: 't5', value: 'internal' },
{ id: 't6', value: 'mobile' },
{ id: 't7', value: 'frontend' },
{ id: 't8', value: 'backend' },
{ id: 't9', value: 'performance' },
{ id: 't10', value: 'accessibility' },
{ id: 't11', value: 'design' },
{ id: 't12', value: 'research' },
{ id: 't13', value: 'testing' },
{ id: 't14', value: 'infrastructure' },
{ id: 't15', value: 'documentation' },
{ id: 'c-accordion', value: 'component: accordion' },
{ id: 'c-alert-dialog', value: 'component: alert dialog' },
{ id: 'c-autocomplete', value: 'component: autocomplete' },
{ id: 'c-avatar', value: 'component: avatar' },
{ id: 'c-checkbox', value: 'component: checkbox' },
{ id: 'c-checkbox-group', value: 'component: checkbox group' },
{ id: 'c-collapsible', value: 'component: collapsible' },
{ id: 'c-combobox', value: 'component: combobox' },
{ id: 'c-context-menu', value: 'component: context menu' },
{ id: 'c-dialog', value: 'component: dialog' },
{ id: 'c-field', value: 'component: field' },
{ id: 'c-fieldset', value: 'component: fieldset' },
{ id: 'c-filterable-menu', value: 'component: filterable menu' },
{ id: 'c-form', value: 'component: form' },
{ id: 'c-input', value: 'component: input' },
{ id: 'c-menu', value: 'component: menu' },
{ id: 'c-menubar', value: 'component: menubar' },
{ id: 'c-meter', value: 'component: meter' },
{ id: 'c-navigation-menu', value: 'component: navigation menu' },
{ id: 'c-number-field', value: 'component: number field' },
{ id: 'c-popover', value: 'component: popover' },
{ id: 'c-preview-card', value: 'component: preview card' },
{ id: 'c-progress', value: 'component: progress' },
{ id: 'c-radio', value: 'component: radio' },
{ id: 'c-scroll-area', value: 'component: scroll area' },
{ id: 'c-select', value: 'component: select' },
{ id: 'c-separator', value: 'component: separator' },
{ id: 'c-slider', value: 'component: slider' },
{ id: 'c-switch', value: 'component: switch' },
{ id: 'c-tabs', value: 'component: tabs' },
{ id: 'c-toast', value: 'component: toast' },
{ id: 'c-toggle', value: 'component: toggle' },
{ id: 'c-toggle-group', value: 'component: toggle group' },
{ id: 'c-toolbar', value: 'component: toolbar' },
{ id: 'c-tooltip', value: 'component: tooltip' },
];
```
##### Auto highlight
The first matching item can be automatically highlighted as the user types by specifying the `autoHighlight` prop on ``. Set the prop's value to `"always"` if the highlight should always be present, such as when the list is rendered inline within a dialog.
The prop can be combined with the `keepHighlight` and `highlightItemOnHover` props to configure how the highlight behaves during mouse interactions.
#### Demo
##### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
'use client';
import { Autocomplete } from '@base-ui/react/autocomplete';
export default function ExampleAutocompleteAutoHighlight() {
return (
Auto highlight on type
No tags found.
{(tag: Tag) => (
{tag.value}
)}
);
}
interface Tag {
id: string;
value: string;
}
const tags: Tag[] = [
{ id: 't1', value: 'feature' },
{ id: 't2', value: 'fix' },
{ id: 't3', value: 'bug' },
{ id: 't4', value: 'docs' },
{ id: 't5', value: 'internal' },
{ id: 't6', value: 'mobile' },
{ id: 'c-accordion', value: 'component: accordion' },
{ id: 'c-alert-dialog', value: 'component: alert dialog' },
{ id: 'c-autocomplete', value: 'component: autocomplete' },
{ id: 'c-avatar', value: 'component: avatar' },
{ id: 'c-checkbox', value: 'component: checkbox' },
{ id: 'c-checkbox-group', value: 'component: checkbox group' },
{ id: 'c-collapsible', value: 'component: collapsible' },
{ id: 'c-combobox', value: 'component: combobox' },
{ id: 'c-context-menu', value: 'component: context menu' },
{ id: 'c-dialog', value: 'component: dialog' },
{ id: 'c-field', value: 'component: field' },
{ id: 'c-fieldset', value: 'component: fieldset' },
{ id: 'c-filterable-menu', value: 'component: filterable menu' },
{ id: 'c-form', value: 'component: form' },
{ id: 'c-input', value: 'component: input' },
{ id: 'c-menu', value: 'component: menu' },
{ id: 'c-menubar', value: 'component: menubar' },
{ id: 'c-meter', value: 'component: meter' },
{ id: 'c-navigation-menu', value: 'component: navigation menu' },
{ id: 'c-number-field', value: 'component: number field' },
{ id: 'c-popover', value: 'component: popover' },
{ id: 'c-preview-card', value: 'component: preview card' },
{ id: 'c-progress', value: 'component: progress' },
{ id: 'c-radio', value: 'component: radio' },
{ id: 'c-scroll-area', value: 'component: scroll area' },
{ id: 'c-select', value: 'component: select' },
{ id: 'c-separator', value: 'component: separator' },
{ id: 'c-slider', value: 'component: slider' },
{ id: 'c-switch', value: 'component: switch' },
{ id: 'c-tabs', value: 'component: tabs' },
{ id: 'c-toast', value: 'component: toast' },
{ id: 'c-toggle', value: 'component: toggle' },
{ id: 'c-toggle-group', value: 'component: toggle group' },
{ id: 'c-toolbar', value: 'component: toolbar' },
{ id: 'c-tooltip', value: 'component: tooltip' },
];
```
##### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Input {
box-sizing: border-box;
padding: 0 0.5rem;
margin: 0;
border-radius: 0;
border: 1px solid oklch(14.5% 0 0deg);
width: 16rem;
height: 2rem;
font-family: inherit;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 400;
background-color: white;
color: oklch(14.5% 0 0deg);
outline: none;
@media (any-pointer: coarse) {
font-size: 1rem;
line-height: 1.5rem;
}
@media (prefers-color-scheme: dark) {
border: 1px solid white;
background-color: oklch(14.5% 0 0deg);
color: white;
}
&::placeholder {
color: oklch(55.6% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
&:focus {
outline: 2px solid oklch(14.5% 0 0deg);
outline-offset: -1px;
@media (prefers-color-scheme: dark) {
outline-color: white;
}
}
}
.Label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 700;
color: oklch(14.5% 0 0deg);
@media (prefers-color-scheme: dark) {
color: white;
}
}
.Positioner {
outline: 0;
}
.Popup {
box-sizing: border-box;
background-color: white;
color: oklch(14.5% 0 0deg);
width: var(--anchor-width);
max-width: var(--available-width);
border: 1px solid oklch(14.5% 0 0deg);
box-shadow: 0.25rem 0.25rem 0 rgb(0 0 0 / 12%);
@media (prefers-color-scheme: dark) {
background-color: oklch(14.5% 0 0deg);
color: white;
border: 1px solid white;
box-shadow: none;
}
}
.List {
box-sizing: border-box;
overflow-y: auto;
overscroll-behavior: contain;
padding-block: 0.25rem;
scroll-padding-block: 0.25rem;
outline: 0;
max-height: min(22.5rem, var(--available-height));
&[data-empty] {
padding: 0;
}
}
.Item {
box-sizing: border-box;
outline: 0;
cursor: default;
-webkit-user-select: none;
user-select: none;
padding-block: 0.5rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
display: flex;
font-size: 0.875rem;
line-height: 1rem;
&[data-highlighted] {
z-index: 0;
position: relative;
color: white;
@media (prefers-color-scheme: dark) {
color: oklch(14.5% 0 0deg);
}
}
&[data-highlighted]::before {
content: '';
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0;
background-color: oklch(14.5% 0 0deg);
@media (prefers-color-scheme: dark) {
background-color: white;
}
}
}
.Empty {
box-sizing: border-box;
padding: 1rem 1rem 1rem 0.5rem;
font-size: 0.875rem;
line-height: 1rem;
color: oklch(55.6% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
```
```tsx
/* index.tsx */
'use client';
import { Autocomplete } from '@base-ui/react/autocomplete';
import styles from './index.module.css';
export default function ExampleAutocompleteAutoHighlight() {
return (
Auto highlight on type
No tags found.
{(tag: Tag) => (
{tag.value}
)}
);
}
interface Tag {
id: string;
value: string;
}
const tags: Tag[] = [
{ id: 't1', value: 'feature' },
{ id: 't2', value: 'fix' },
{ id: 't3', value: 'bug' },
{ id: 't4', value: 'docs' },
{ id: 't5', value: 'internal' },
{ id: 't6', value: 'mobile' },
{ id: 'c-accordion', value: 'component: accordion' },
{ id: 'c-alert-dialog', value: 'component: alert dialog' },
{ id: 'c-autocomplete', value: 'component: autocomplete' },
{ id: 'c-avatar', value: 'component: avatar' },
{ id: 'c-checkbox', value: 'component: checkbox' },
{ id: 'c-checkbox-group', value: 'component: checkbox group' },
{ id: 'c-collapsible', value: 'component: collapsible' },
{ id: 'c-combobox', value: 'component: combobox' },
{ id: 'c-context-menu', value: 'component: context menu' },
{ id: 'c-dialog', value: 'component: dialog' },
{ id: 'c-field', value: 'component: field' },
{ id: 'c-fieldset', value: 'component: fieldset' },
{ id: 'c-filterable-menu', value: 'component: filterable menu' },
{ id: 'c-form', value: 'component: form' },
{ id: 'c-input', value: 'component: input' },
{ id: 'c-menu', value: 'component: menu' },
{ id: 'c-menubar', value: 'component: menubar' },
{ id: 'c-meter', value: 'component: meter' },
{ id: 'c-navigation-menu', value: 'component: navigation menu' },
{ id: 'c-number-field', value: 'component: number field' },
{ id: 'c-popover', value: 'component: popover' },
{ id: 'c-preview-card', value: 'component: preview card' },
{ id: 'c-progress', value: 'component: progress' },
{ id: 'c-radio', value: 'component: radio' },
{ id: 'c-scroll-area', value: 'component: scroll area' },
{ id: 'c-select', value: 'component: select' },
{ id: 'c-separator', value: 'component: separator' },
{ id: 'c-slider', value: 'component: slider' },
{ id: 'c-switch', value: 'component: switch' },
{ id: 'c-tabs', value: 'component: tabs' },
{ id: 'c-toast', value: 'component: toast' },
{ id: 'c-toggle', value: 'component: toggle' },
{ id: 'c-toggle-group', value: 'component: toggle group' },
{ id: 'c-toolbar', value: 'component: toolbar' },
{ id: 'c-tooltip', value: 'component: tooltip' },
];
```
##### Command palette
Use the autocomplete input to filter a list of command items that perform an action when clicked.
#### Demo
##### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
'use client';
import * as React from 'react';
import { Autocomplete } from '@base-ui/react/autocomplete';
import { Dialog } from '@base-ui/react/dialog';
import { ScrollArea } from '@base-ui/react/scroll-area';
export default function ExampleAutocompleteCommandPalette() {
const [open, setOpen] = React.useState(false);
const shortcutsDescriptionId = React.useId();
function handleItemClick() {
setOpen(false);
}
return (
Open command palette
Close command palette
No results found.
{(group: Group) => (
{group.value}
{(item: Item) => (
{item.label}
{group.value === 'Suggestions' ? 'Application' : 'Command'}
)}
)}
Use Enter to activate the highlighted item.
Activate
Enter
);
}
interface Item {
value: string;
label: string;
}
interface Group {
value: string;
items: Item[];
}
const suggestions: Item[] = [
{ value: 'linear', label: 'Linear' },
{ value: 'figma', label: 'Figma' },
{ value: 'slack', label: 'Slack' },
{ value: 'youtube', label: 'YouTube' },
{ value: 'raycast', label: 'Raycast' },
{ value: 'notion', label: 'Notion' },
{ value: 'github', label: 'GitHub' },
{ value: 'jira', label: 'Jira' },
{ value: 'calendar', label: 'Google Calendar' },
{ value: 'chrome', label: 'Google Chrome' },
{ value: 'mail', label: 'Apple Mail' },
{ value: 'terminal', label: 'Terminal' },
];
const commands: Item[] = [
{ value: 'clipboard-history', label: 'Clipboard History' },
{ value: 'import-extension', label: 'Import Extension' },
{ value: 'create-snippet', label: 'Create Snippet' },
{ value: 'system-preferences', label: 'System Preferences' },
{ value: 'window-management', label: 'Window Management' },
{ value: 'toggle-dark-mode', label: 'Toggle Dark Mode' },
{ value: 'new-window', label: 'New Window' },
{ value: 'new-tab', label: 'New Tab' },
{ value: 'search-docs', label: 'Search Documentation' },
{ value: 'capture-screen', label: 'Capture Screenshot' },
{ value: 'close-sidebar', label: 'Toggle Sidebar' },
{ value: 'toggle-terminal', label: 'Toggle Integrated Terminal' },
{ value: 'run-script', label: 'Run Script' },
];
const groupedItems: Group[] = [
{ value: 'Suggestions', items: suggestions },
{ value: 'Commands', items: commands },
];
```
##### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
height: 2rem;
padding: 0 0.75rem;
margin: 0;
outline: 0;
border: 1px solid oklch(14.5% 0 0deg);
background-color: white;
font-family: inherit;
font-size: 0.875rem;
font-weight: 400;
line-height: 1;
white-space: nowrap;
color: oklch(14.5% 0 0deg);
-webkit-user-select: none;
user-select: none;
cursor: default;
@media (prefers-color-scheme: dark) {
border: 1px solid white;
background-color: oklch(14.5% 0 0deg);
color: white;
}
@media (hover: hover) {
&:hover {
background-color: oklch(97% 0 0deg);
@media (prefers-color-scheme: dark) {
background-color: oklch(26.9% 0 0deg);
}
}
}
&:active {
background-color: oklch(92.2% 0 0deg);
@media (prefers-color-scheme: dark) {
background-color: oklch(37.1% 0 0deg);
}
}
&:focus-visible {
outline: 2px solid oklch(14.5% 0 0deg);
outline-offset: -1px;
@media (prefers-color-scheme: dark) {
outline-color: white;
}
}
}
.Backdrop {
position: fixed;
inset: 0;
background-color: black;
opacity: 0.2;
transition: opacity 150ms cubic-bezier(0.45, 1.005, 0, 1.005);
@media (prefers-color-scheme: dark) {
opacity: 0.7;
}
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
}
@supports (-webkit-touch-callout: none) {
position: absolute;
}
}
.Viewport {
position: fixed;
inset: 0;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 4.5rem 0.5rem 0.5rem;
overflow: hidden;
}
.Popup {
box-sizing: border-box;
position: relative;
width: calc(100vw - 1rem);
max-width: 28rem;
background-color: white;
color: oklch(14.5% 0 0deg);
transition:
transform 150ms,
opacity 150ms;
max-height: min(36rem, calc(100dvh - 5rem));
display: flex;
flex-direction: column;
border: 1px solid oklch(14.5% 0 0deg);
box-shadow: 0.25rem 0.25rem 0 rgb(0 0 0 / 12%);
@media (prefers-color-scheme: dark) {
background-color: oklch(14.5% 0 0deg);
color: white;
border: 1px solid white;
box-shadow: none;
}
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: translateY(-1rem) scale(0.95);
}
}
.Input {
box-sizing: border-box;
position: relative;
z-index: 1;
padding: 0 0.75rem;
margin: 0;
border: none;
width: 100%;
height: 2.5rem;
border-radius: 0;
font-family: inherit;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 400;
background-color: white;
color: oklch(14.5% 0 0deg);
outline: none;
@media (any-pointer: coarse) {
font-size: 1rem;
line-height: 1.5rem;
}
@media (prefers-color-scheme: dark) {
background-color: oklch(14.5% 0 0deg);
color: white;
}
&::placeholder {
color: oklch(55.6% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
&:focus {
outline: 2px solid oklch(14.5% 0 0deg);
@media (prefers-color-scheme: dark) {
outline-color: white;
}
}
}
.ListArea {
position: relative;
display: flex;
flex: 0 1 auto;
min-height: 0;
max-height: min(60dvh, 24rem);
overflow: hidden;
border-top: 1px solid oklch(14.5% 0 0deg);
@media (prefers-color-scheme: dark) {
border-color: white;
}
}
.ListViewport {
box-sizing: border-box;
flex: 1 1 auto;
min-height: 0;
overscroll-behavior: contain;
scroll-padding-block: 0.25rem;
&:focus-visible {
outline: 2px solid oklch(14.5% 0 0deg);
outline-offset: -1px;
@media (prefers-color-scheme: dark) {
outline-color: white;
}
}
}
.List {
box-sizing: border-box;
padding: 0.25rem 0;
&:empty {
padding: 0;
}
}
.ListContent {
min-width: 100%;
}
.Group:not(:last-child) {
margin-block-end: 0.25rem;
}
.GroupLabel {
outline: 0;
-webkit-user-select: none;
user-select: none;
display: flex;
align-items: center;
min-height: 2rem;
margin: 0;
padding: 0 1.5rem 0 0.75rem;
font-size: 0.875rem;
line-height: 1;
font-weight: 400;
color: oklch(55.6% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
.Item {
box-sizing: border-box;
outline: 0;
cursor: default;
-webkit-user-select: none;
user-select: none;
scroll-margin-block: 0.25rem;
min-height: 2rem;
padding: 0 1.5rem;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.5rem;
align-items: center;
font-size: 0.875rem;
line-height: 1.25;
font-weight: 400;
&[data-highlighted] {
background-color: oklch(92.2% 0 0deg);
@media (prefers-color-scheme: dark) {
background-color: oklch(37.1% 0 0deg);
}
}
}
.ItemLabel {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 400;
}
.ItemType {
font-size: 0.875rem;
color: oklch(55.6% 0 0deg);
white-space: nowrap;
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
[data-highlighted] & {
color: oklch(37.1% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(87% 0 0deg);
}
}
}
.Empty {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: flex-start;
padding: 1rem 1rem 1rem 0.5rem;
font-size: 0.875rem;
line-height: 1rem;
color: oklch(55.6% 0 0deg);
min-height: 8rem;
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
.Scrollbar {
display: flex;
justify-content: center;
background-color: rgb(0 0 0 / 12%);
width: 1rem;
transition: opacity 150ms;
@media (prefers-color-scheme: dark) {
background-color: rgb(255 255 255 / 12%);
}
}
.ScrollbarThumb {
width: 100%;
background-color: oklch(14.5% 0 0deg);
@media (prefers-color-scheme: dark) {
background-color: white;
}
}
.Footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.625rem 0.75rem;
border-top: 1px solid oklch(14.5% 0 0deg);
font-size: 0.75rem;
color: oklch(43.9% 0 0deg);
background-color: white;
@media (prefers-color-scheme: dark) {
border-top: 1px solid white;
color: oklch(70.8% 0 0deg);
background-color: oklch(14.5% 0 0deg);
}
}
.FooterLeft,
.FooterRight {
display: flex;
align-items: center;
gap: 0.25rem;
}
.Kbd {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.25rem;
height: 1.25rem;
padding: 0 0.25rem;
font-size: 0.625rem;
font-family:
ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
font-weight: 400;
line-height: 1;
color: oklch(43.9% 0 0deg);
background-color: oklch(97% 0 0deg);
border: 1px solid oklch(70.8% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
background-color: oklch(20.5% 0 0deg);
border: 1px solid oklch(43.9% 0 0deg);
}
}
.VisuallyHidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
```
```tsx
/* index.tsx */
'use client';
import * as React from 'react';
import { Dialog } from '@base-ui/react/dialog';
import { Autocomplete } from '@base-ui/react/autocomplete';
import { ScrollArea } from '@base-ui/react/scroll-area';
import styles from './index.module.css';
export default function ExampleAutocompleteCommandPalette() {
const [open, setOpen] = React.useState(false);
const shortcutsDescriptionId = React.useId();
function handleItemClick() {
setOpen(false);
}
return (
Open command palette
Close command palette
No results found.
{(group: Group) => (
{group.value}
{(item: Item) => (
{item.label}
{group.value === 'Suggestions' ? 'Application' : 'Command'}
)}
)}
Use Enter to activate the highlighted item.
Activate
Enter
);
}
interface Item {
value: string;
label: string;
}
interface Group {
value: string;
items: Item[];
}
const suggestions: Item[] = [
{ value: 'linear', label: 'Linear' },
{ value: 'figma', label: 'Figma' },
{ value: 'slack', label: 'Slack' },
{ value: 'youtube', label: 'YouTube' },
{ value: 'raycast', label: 'Raycast' },
{ value: 'notion', label: 'Notion' },
{ value: 'github', label: 'GitHub' },
{ value: 'jira', label: 'Jira' },
{ value: 'calendar', label: 'Google Calendar' },
{ value: 'chrome', label: 'Google Chrome' },
{ value: 'mail', label: 'Apple Mail' },
{ value: 'terminal', label: 'Terminal' },
];
const commands: Item[] = [
{ value: 'clipboard-history', label: 'Clipboard History' },
{ value: 'import-extension', label: 'Import Extension' },
{ value: 'create-snippet', label: 'Create Snippet' },
{ value: 'system-preferences', label: 'System Preferences' },
{ value: 'window-management', label: 'Window Management' },
{ value: 'toggle-dark-mode', label: 'Toggle Dark Mode' },
{ value: 'new-window', label: 'New Window' },
{ value: 'new-tab', label: 'New Tab' },
{ value: 'search-docs', label: 'Search Documentation' },
{ value: 'capture-screen', label: 'Capture Screenshot' },
{ value: 'close-sidebar', label: 'Toggle Sidebar' },
{ value: 'toggle-terminal', label: 'Toggle Integrated Terminal' },
{ value: 'run-script', label: 'Run Script' },
];
const groupedItems: Group[] = [
{ value: 'Suggestions', items: suggestions },
{ value: 'Commands', items: commands },
];
```
##### Grid layout
Display items in a grid layout, wrapping each row in `` components.
#### Demo
##### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
'use client';
import * as React from 'react';
import { Autocomplete } from '@base-ui/react/autocomplete';
export default function ExampleEmojiPicker() {
const [pickerOpen, setPickerOpen] = React.useState(false);
const [textValue, setTextValue] = React.useState('');
const [searchValue, setSearchValue] = React.useState('');
const textInputRef = React.useRef(null);
function handleInsertEmoji(value: string | null) {
if (!value || !textInputRef.current) {
return;
}
const emoji = value;
const start = textInputRef.current.selectionStart ?? textInputRef.current.value.length ?? 0;
const end = textInputRef.current.selectionEnd ?? textInputRef.current.value.length ?? 0;
setTextValue((prev) => prev.slice(0, start) + emoji + prev.slice(end));
setPickerOpen(false);
const input = textInputRef.current;
if (input) {
input.focus();
const caretPos = start + emoji.length;
input.setSelectionRange(caretPos, caretPos);
}
}
return (
setTextValue(event.target.value)}
/>
setSearchValue('')}
value={searchValue}
onValueChange={(value, details) => {
if (details.reason !== 'item-press') {
setSearchValue(value);
}
}}
>
😀
No emojis found
{(group: EmojiGroup) => (
{group.label}
{chunkArray(group.items, COLUMNS).map((row, rowIdx) => (
{row.map((rowItem) => (
{
handleInsertEmoji(rowItem.emoji);
setPickerOpen(false);
}}
>
{rowItem.emoji}
))}
))}
)}
);
}
const COLUMNS = 5;
function chunkArray(array: T[], size: number): T[][] {
const result: T[][] = [];
for (let i = 0; i < array.length; i += size) {
result.push(array.slice(i, i + size));
}
return result;
}
interface EmojiItem {
emoji: string;
value: string;
name: string;
}
interface EmojiGroup {
value: string;
label: string;
items: EmojiItem[];
}
export const emojiCategories = [
{
label: 'Smileys & Emotion',
emojis: [
{ emoji: '😀', name: 'grinning face' },
{ emoji: '😃', name: 'grinning face with big eyes' },
{ emoji: '😄', name: 'grinning face with smiling eyes' },
{ emoji: '😁', name: 'beaming face with smiling eyes' },
{ emoji: '😆', name: 'grinning squinting face' },
{ emoji: '😅', name: 'grinning face with sweat' },
{ emoji: '🤣', name: 'rolling on the floor laughing' },
{ emoji: '😂', name: 'face with tears of joy' },
{ emoji: '🙂', name: 'slightly smiling face' },
{ emoji: '🙃', name: 'upside-down face' },
{ emoji: '😉', name: 'winking face' },
{ emoji: '😊', name: 'smiling face with smiling eyes' },
{ emoji: '😇', name: 'smiling face with halo' },
{ emoji: '🥰', name: 'smiling face with hearts' },
{ emoji: '😍', name: 'smiling face with heart-eyes' },
{ emoji: '🤩', name: 'star-struck' },
{ emoji: '😘', name: 'face blowing a kiss' },
{ emoji: '😗', name: 'kissing face' },
{ emoji: '☺️', name: 'smiling face' },
{ emoji: '😚', name: 'kissing face with closed eyes' },
{ emoji: '😙', name: 'kissing face with smiling eyes' },
{ emoji: '🥲', name: 'smiling face with tear' },
{ emoji: '😋', name: 'face savoring food' },
{ emoji: '😛', name: 'face with tongue' },
{ emoji: '😜', name: 'winking face with tongue' },
{ emoji: '🤪', name: 'zany face' },
{ emoji: '😝', name: 'squinting face with tongue' },
{ emoji: '🤑', name: 'money-mouth face' },
{ emoji: '🤗', name: 'hugging face' },
{ emoji: '🤭', name: 'face with hand over mouth' },
],
},
{
label: 'Animals & Nature',
emojis: [
{ emoji: '🐶', name: 'dog face' },
{ emoji: '🐱', name: 'cat face' },
{ emoji: '🐭', name: 'mouse face' },
{ emoji: '🐹', name: 'hamster' },
{ emoji: '🐰', name: 'rabbit face' },
{ emoji: '🦊', name: 'fox' },
{ emoji: '🐻', name: 'bear' },
{ emoji: '🐼', name: 'panda' },
{ emoji: '🐨', name: 'koala' },
{ emoji: '🐯', name: 'tiger face' },
{ emoji: '🦁', name: 'lion' },
{ emoji: '🐮', name: 'cow face' },
{ emoji: '🐷', name: 'pig face' },
{ emoji: '🐽', name: 'pig nose' },
{ emoji: '🐸', name: 'frog' },
{ emoji: '🐵', name: 'monkey face' },
{ emoji: '🙈', name: 'see-no-evil monkey' },
{ emoji: '🙉', name: 'hear-no-evil monkey' },
{ emoji: '🙊', name: 'speak-no-evil monkey' },
{ emoji: '🐒', name: 'monkey' },
{ emoji: '🐔', name: 'chicken' },
{ emoji: '🐧', name: 'penguin' },
{ emoji: '🐦', name: 'bird' },
{ emoji: '🐤', name: 'baby chick' },
{ emoji: '🐣', name: 'hatching chick' },
{ emoji: '🐥', name: 'front-facing baby chick' },
{ emoji: '🦆', name: 'duck' },
{ emoji: '🦅', name: 'eagle' },
{ emoji: '🦉', name: 'owl' },
{ emoji: '🦇', name: 'bat' },
],
},
{
label: 'Food & Drink',
emojis: [
{ emoji: '🍎', name: 'red apple' },
{ emoji: '🍏', name: 'green apple' },
{ emoji: '🍊', name: 'tangerine' },
{ emoji: '🍋', name: 'lemon' },
{ emoji: '🍌', name: 'banana' },
{ emoji: '🍉', name: 'watermelon' },
{ emoji: '🍇', name: 'grapes' },
{ emoji: '🍓', name: 'strawberry' },
{ emoji: '🫐', name: 'blueberries' },
{ emoji: '🍈', name: 'melon' },
{ emoji: '🍒', name: 'cherries' },
{ emoji: '🍑', name: 'peach' },
{ emoji: '🥭', name: 'mango' },
{ emoji: '🍍', name: 'pineapple' },
{ emoji: '🥥', name: 'coconut' },
{ emoji: '🥝', name: 'kiwi fruit' },
{ emoji: '🍅', name: 'tomato' },
{ emoji: '🍆', name: 'eggplant' },
{ emoji: '🥑', name: 'avocado' },
{ emoji: '🥦', name: 'broccoli' },
{ emoji: '🥬', name: 'leafy greens' },
{ emoji: '🥒', name: 'cucumber' },
{ emoji: '🌶️', name: 'hot pepper' },
{ emoji: '🫑', name: 'bell pepper' },
{ emoji: '🌽', name: 'ear of corn' },
{ emoji: '🥕', name: 'carrot' },
{ emoji: '🫒', name: 'olive' },
{ emoji: '🧄', name: 'garlic' },
{ emoji: '🧅', name: 'onion' },
{ emoji: '🥔', name: 'potato' },
],
},
];
const emojiGroups: EmojiGroup[] = emojiCategories.map((category) => ({
value: category.label,
label: category.label,
items: category.emojis.map((emoji) => ({
...emoji,
value: emoji.name.toLowerCase(),
})),
}));
```
##### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Container {
width: 16rem;
margin: 0 auto;
}
.InputGroup {
position: relative;
display: flex;
width: 100%;
}
.TextInput {
box-sizing: border-box;
flex: 1;
padding: 0 0.5rem;
margin: 0;
margin-right: -1px;
border-radius: 0;
border: 1px solid oklch(14.5% 0 0deg);
border-right: 0;
height: 2rem;
font-family: inherit;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 400;
color: oklch(14.5% 0 0deg);
outline: none;
background-color: white;
@media (any-pointer: coarse) {
font-size: 1rem;
line-height: 1.5rem;
}
@media (prefers-color-scheme: dark) {
border: 1px solid white;
border-right: 0;
color: white;
background-color: oklch(14.5% 0 0deg);
}
&::placeholder {
color: oklch(55.6% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
&:focus {
position: relative;
outline: 2px solid oklch(14.5% 0 0deg);
outline-offset: -1px;
@media (prefers-color-scheme: dark) {
outline-color: white;
}
}
}
.EmojiButton {
box-sizing: border-box;
width: 2rem;
height: 2rem;
border: 1px solid oklch(14.5% 0 0deg);
background-color: transparent;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
line-height: 1;
color: oklch(14.5% 0 0deg);
outline: none;
@media (prefers-color-scheme: dark) {
border: 1px solid white;
color: white;
}
&:hover {
background-color: oklch(97% 0 0deg);
@media (prefers-color-scheme: dark) {
background-color: oklch(26.9% 0 0deg);
}
}
&:active {
background-color: oklch(92.2% 0 0deg);
@media (prefers-color-scheme: dark) {
background-color: oklch(37.1% 0 0deg);
}
}
&[data-popup-open] {
background-color: oklch(97% 0 0deg);
@media (prefers-color-scheme: dark) {
background-color: oklch(26.9% 0 0deg);
}
}
&:focus-visible {
outline: 2px solid oklch(14.5% 0 0deg);
outline-offset: -1px;
@media (prefers-color-scheme: dark) {
outline-color: white;
}
}
}
.Trigger {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
height: 2.5rem;
padding-left: 0.875rem;
padding-right: 0.75rem;
margin: 0;
outline: 0;
border: 1px solid oklch(14.5% 0 0deg);
font-family: inherit;
font-size: 1rem;
line-height: 1.5rem;
color: oklch(14.5% 0 0deg);
cursor: default;
-webkit-user-select: none;
user-select: none;
min-width: 9rem;
background-color: white;
@media (prefers-color-scheme: dark) {
border: 1px solid white;
color: white;
background-color: oklch(14.5% 0 0deg);
}
@media (hover: hover) {
&:hover {
background-color: oklch(97% 0 0deg);
@media (prefers-color-scheme: dark) {
background-color: oklch(26.9% 0 0deg);
}
}
}
&[data-popup-open] {
background-color: oklch(97% 0 0deg);
@media (prefers-color-scheme: dark) {
background-color: oklch(26.9% 0 0deg);
}
}
&:focus-visible {
outline: 2px solid oklch(14.5% 0 0deg);
outline-offset: -1px;
@media (prefers-color-scheme: dark) {
outline-color: white;
}
}
}
.Input {
box-sizing: border-box;
padding: 0 0.5rem;
border: 1px solid oklch(14.5% 0 0deg);
border-radius: 0;
margin: 0;
width: 16rem;
max-width: 100%;
height: var(--input-container-height);
font-family: inherit;
font-size: 0.875rem;
line-height: 1.25rem;
background-color: white;
color: oklch(14.5% 0 0deg);
outline: none;
@media (any-pointer: coarse) {
font-size: 1rem;
line-height: 1.5rem;
}
@media (prefers-color-scheme: dark) {
background-color: oklch(14.5% 0 0deg);
color: white;
border-color: white;
}
&::placeholder {
color: oklch(55.6% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
&:focus {
outline: 2px solid oklch(14.5% 0 0deg);
outline-offset: -2px;
@media (prefers-color-scheme: dark) {
outline-color: white;
}
}
}
.Viewport {
border: 1px solid oklch(14.5% 0 0deg);
border-top: none;
@media (prefers-color-scheme: dark) {
border-color: white;
}
}
.Positioner {
outline: 0;
}
.Popup {
--input-container-height: 2rem;
box-sizing: border-box;
background-color: white;
color: oklch(14.5% 0 0deg);
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;
max-width: var(--available-width);
max-height: 20.5rem;
box-shadow: 0.25rem 0.25rem 0 rgb(0 0 0 / 12%);
@media (prefers-color-scheme: dark) {
background-color: oklch(14.5% 0 0deg);
color: white;
box-shadow: none;
}
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
}
.List {
overflow: auto;
scroll-padding-top: 0.25rem;
scroll-padding-bottom: 0.35rem;
overscroll-behavior: contain;
max-height: min(
calc(20.5rem - var(--input-container-height) - 2px),
calc(var(--available-height) - var(--input-container-height) - 2px)
);
padding-block: 0.5rem;
&:empty {
padding: 0;
}
}
.GroupLabel {
box-sizing: border-box;
padding: 0.5rem;
font-size: 0.875rem;
line-height: 1rem;
color: oklch(55.6% 0 0deg);
-webkit-user-select: none;
user-select: none;
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
.Group {
display: block;
}
.Grid {
padding: 0 0.5rem 0.25rem;
}
.Row {
display: grid;
grid-template-columns: repeat(var(--cols, 5), 1fr);
}
.Item {
box-sizing: border-box;
outline: 0;
cursor: default;
-webkit-user-select: none;
user-select: none;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: var(--anchor-width);
height: 2.5rem;
padding: 0.5rem 0.125rem;
background: transparent;
&[data-highlighted] {
z-index: 0;
position: relative;
color: white;
@media (prefers-color-scheme: dark) {
color: oklch(14.5% 0 0deg);
}
&::before {
content: '';
z-index: -1;
position: absolute;
inset: 0;
background-color: oklch(97% 0 0deg);
@media (prefers-color-scheme: dark) {
background-color: oklch(26.9% 0 0deg);
}
}
}
}
.Emoji {
font-size: 1.5rem;
line-height: 1;
}
.Name {
font-size: 0.625rem;
text-align: center;
opacity: 0.8;
line-height: 1.2;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
}
.Item[data-highlighted] .Name {
opacity: 1;
}
.Empty {
box-sizing: border-box;
padding: 0.75rem 0.5rem;
font-size: 0.875rem;
line-height: 1rem;
color: oklch(55.6% 0 0deg);
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
```
```tsx
/* index.tsx */
'use client';
import * as React from 'react';
import { Autocomplete } from '@base-ui/react/autocomplete';
import styles from './index.module.css';
export default function ExampleEmojiPicker() {
const [pickerOpen, setPickerOpen] = React.useState(false);
const [textValue, setTextValue] = React.useState('');
const [searchValue, setSearchValue] = React.useState('');
const textInputRef = React.useRef(null);
function handleInsertEmoji(value: string | null) {
if (!value || !textInputRef.current) {
return;
}
const emoji = value;
const start = textInputRef.current.selectionStart ?? textInputRef.current.value.length ?? 0;
const end = textInputRef.current.selectionEnd ?? textInputRef.current.value.length ?? 0;
setTextValue((prev) => prev.slice(0, start) + emoji + prev.slice(end));
setPickerOpen(false);
const input = textInputRef.current;
if (input) {
input.focus();
const caretPos = start + emoji.length;
input.setSelectionRange(caretPos, caretPos);
}
}
return (
setTextValue(event.target.value)}
/>
setSearchValue('')}
value={searchValue}
onValueChange={(value, details) => {
if (details.reason !== 'item-press') {
setSearchValue(value);
}
}}
>
😀
No emojis found
{(group: EmojiGroup) => (
{group.label}
{chunkArray(group.items, COLUMNS).map((row, rowIdx) => (
{row.map((rowItem) => (
{
handleInsertEmoji(rowItem.emoji);
setPickerOpen(false);
}}
>
{rowItem.emoji}
))}
))}
)}
);
}
const COLUMNS = 5;
function chunkArray(array: T[], size: number): T[][] {
const result: T[][] = [];
for (let i = 0; i < array.length; i += size) {
result.push(array.slice(i, i + size));
}
return result;
}
interface EmojiItem {
emoji: string;
value: string;
name: string;
}
interface EmojiGroup {
value: string;
label: string;
items: EmojiItem[];
}
export const emojiCategories = [
{
label: 'Smileys & Emotion',
emojis: [
{ emoji: '😀', name: 'grinning face' },
{ emoji: '😃', name: 'grinning face with big eyes' },
{ emoji: '😄', name: 'grinning face with smiling eyes' },
{ emoji: '😁', name: 'beaming face with smiling eyes' },
{ emoji: '😆', name: 'grinning squinting face' },
{ emoji: '😅', name: 'grinning face with sweat' },
{ emoji: '🤣', name: 'rolling on the floor laughing' },
{ emoji: '😂', name: 'face with tears of joy' },
{ emoji: '🙂', name: 'slightly smiling face' },
{ emoji: '🙃', name: 'upside-down face' },
{ emoji: '😉', name: 'winking face' },
{ emoji: '😊', name: 'smiling face with smiling eyes' },
{ emoji: '😇', name: 'smiling face with halo' },
{ emoji: '🥰', name: 'smiling face with hearts' },
{ emoji: '😍', name: 'smiling face with heart-eyes' },
{ emoji: '🤩', name: 'star-struck' },
{ emoji: '😘', name: 'face blowing a kiss' },
{ emoji: '😗', name: 'kissing face' },
{ emoji: '☺️', name: 'smiling face' },
{ emoji: '😚', name: 'kissing face with closed eyes' },
{ emoji: '😙', name: 'kissing face with smiling eyes' },
{ emoji: '🥲', name: 'smiling face with tear' },
{ emoji: '😋', name: 'face savoring food' },
{ emoji: '😛', name: 'face with tongue' },
{ emoji: '😜', name: 'winking face with tongue' },
{ emoji: '🤪', name: 'zany face' },
{ emoji: '😝', name: 'squinting face with tongue' },
{ emoji: '🤑', name: 'money-mouth face' },
{ emoji: '🤗', name: 'hugging face' },
{ emoji: '🤭', name: 'face with hand over mouth' },
],
},
{
label: 'Animals & Nature',
emojis: [
{ emoji: '🐶', name: 'dog face' },
{ emoji: '🐱', name: 'cat face' },
{ emoji: '🐭', name: 'mouse face' },
{ emoji: '🐹', name: 'hamster' },
{ emoji: '🐰', name: 'rabbit face' },
{ emoji: '🦊', name: 'fox' },
{ emoji: '🐻', name: 'bear' },
{ emoji: '🐼', name: 'panda' },
{ emoji: '🐨', name: 'koala' },
{ emoji: '🐯', name: 'tiger face' },
{ emoji: '🦁', name: 'lion' },
{ emoji: '🐮', name: 'cow face' },
{ emoji: '🐷', name: 'pig face' },
{ emoji: '🐽', name: 'pig nose' },
{ emoji: '🐸', name: 'frog' },
{ emoji: '🐵', name: 'monkey face' },
{ emoji: '🙈', name: 'see-no-evil monkey' },
{ emoji: '🙉', name: 'hear-no-evil monkey' },
{ emoji: '🙊', name: 'speak-no-evil monkey' },
{ emoji: '🐒', name: 'monkey' },
{ emoji: '🐔', name: 'chicken' },
{ emoji: '🐧', name: 'penguin' },
{ emoji: '🐦', name: 'bird' },
{ emoji: '🐤', name: 'baby chick' },
{ emoji: '🐣', name: 'hatching chick' },
{ emoji: '🐥', name: 'front-facing baby chick' },
{ emoji: '🦆', name: 'duck' },
{ emoji: '🦅', name: 'eagle' },
{ emoji: '🦉', name: 'owl' },
{ emoji: '🦇', name: 'bat' },
],
},
{
label: 'Food & Drink',
emojis: [
{ emoji: '🍎', name: 'red apple' },
{ emoji: '🍏', name: 'green apple' },
{ emoji: '🍊', name: 'tangerine' },
{ emoji: '🍋', name: 'lemon' },
{ emoji: '🍌', name: 'banana' },
{ emoji: '🍉', name: 'watermelon' },
{ emoji: '🍇', name: 'grapes' },
{ emoji: '🍓', name: 'strawberry' },
{ emoji: '🫐', name: 'blueberries' },
{ emoji: '🍈', name: 'melon' },
{ emoji: '🍒', name: 'cherries' },
{ emoji: '🍑', name: 'peach' },
{ emoji: '🥭', name: 'mango' },
{ emoji: '🍍', name: 'pineapple' },
{ emoji: '🥥', name: 'coconut' },
{ emoji: '🥝', name: 'kiwi fruit' },
{ emoji: '🍅', name: 'tomato' },
{ emoji: '🍆', name: 'eggplant' },
{ emoji: '🥑', name: 'avocado' },
{ emoji: '🥦', name: 'broccoli' },
{ emoji: '🥬', name: 'leafy greens' },
{ emoji: '🥒', name: 'cucumber' },
{ emoji: '🌶️', name: 'hot pepper' },
{ emoji: '🫑', name: 'bell pepper' },
{ emoji: '🌽', name: 'ear of corn' },
{ emoji: '🥕', name: 'carrot' },
{ emoji: '🫒', name: 'olive' },
{ emoji: '🧄', name: 'garlic' },
{ emoji: '🧅', name: 'onion' },
{ emoji: '🥔', name: 'potato' },
],
},
];
const emojiGroups: EmojiGroup[] = emojiCategories.map((category) => ({
value: category.label,
label: category.label,
items: category.emojis.map((emoji) => ({
...emoji,
value: emoji.name.toLowerCase(),
})),
}));
```
##### Virtualized
Efficiently handle large datasets using a virtualization library like `@tanstack/react-virtual`.
#### Demo
##### Tailwind
This example shows how to implement the component using Tailwind CSS.
```tsx
/* index.tsx */
'use client';
import * as React from 'react';
import { Autocomplete } from '@base-ui/react/autocomplete';
import { useVirtualizer } from '@tanstack/react-virtual';
export default function ExampleVirtualizedAutocomplete() {
const virtualizerRef = React.useRef(null);
return (
{
const virtualizer = virtualizerRef.current;
if (!item || !virtualizer) {
return;
}
const isStart = index === 0;
const isEnd = index === virtualizer.options.count - 1;
const shouldScroll = reason === 'none' || (reason === 'keyboard' && (isStart || isEnd));
if (shouldScroll) {
queueMicrotask(() => {
virtualizer.scrollToIndex(index, { align: isEnd ? 'start' : 'end' });
});
}
}}
>
Search 10,000 items
No items found.
);
}
function VirtualizedList({
virtualizerRef,
}: {
virtualizerRef: React.RefObject;
}) {
const filteredItems = Autocomplete.useFilteredItems();
const scrollElementRef = React.useRef(null);
const virtualizer = useVirtualizer({
count: filteredItems.length,
getScrollElement: () => scrollElementRef.current,
estimateSize: () => 32,
overscan: 20,
paddingStart: 4,
paddingEnd: 4,
scrollPaddingEnd: 4,
scrollPaddingStart: 4,
});
React.useImperativeHandle(virtualizerRef, () => virtualizer);
const handleScrollElementRef = React.useCallback(
(element: HTMLDivElement | null) => {
scrollElementRef.current = element;
if (element) {
virtualizer.measure();
}
},
[virtualizer],
);
const totalSize = virtualizer.getTotalSize();
if (!filteredItems.length) {
return null;
}
return (
{virtualizer.getVirtualItems().map((virtualItem) => {
const item = filteredItems[virtualItem.index];
if (!item) {
return null;
}
return (
{item.name}
);
})}
);
}
interface VirtualizedItem {
id: string;
name: string;
}
function getItemLabel(item: VirtualizedItem | null) {
return item ? item.name : '';
}
const virtualizedItems: VirtualizedItem[] = Array.from({ length: 10000 }, (_, index) => {
const id = String(index + 1);
const indexLabel = id.padStart(4, '0');
return { id, name: `Item ${indexLabel}` };
});
type Virtualizer = ReturnType>;
```
##### CSS Modules
This example shows how to implement the component using CSS Modules.
```css
/* index.module.css */
.Input {
box-sizing: border-box;
padding: 0 0.5rem;
margin: 0;
border-radius: 0;
border: 1px solid oklch(14.5% 0 0deg);
width: 16rem;
height: 2rem;
font-family: inherit;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 400;
background-color: white;
color: oklch(14.5% 0 0deg);
outline: none;
@media (any-pointer: coarse) {
font-size: 1rem;
line-height: 1.5rem;
}
@media (prefers-color-scheme: dark) {
border: 1px solid white;
background-color: oklch(14.5% 0 0deg);
color: white;
}
&:focus {
outline: 2px solid oklch(14.5% 0 0deg);
outline-offset: -1px;
@media (prefers-color-scheme: dark) {
outline-color: white;
}
}
}
.Label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 700;
color: oklch(14.5% 0 0deg);
@media (prefers-color-scheme: dark) {
color: white;
}
}
.Positioner {
outline: 0;
}
.Popup {
box-sizing: border-box;
background-color: white;
color: oklch(14.5% 0 0deg);
width: var(--anchor-width);
max-width: var(--available-width);
border: 1px solid oklch(14.5% 0 0deg);
box-shadow: 0.25rem 0.25rem 0 rgb(0 0 0 / 12%);
@media (prefers-color-scheme: dark) {
background-color: oklch(14.5% 0 0deg);
color: white;
border: 1px solid white;
box-shadow: none;
}
}
.Scroller {
box-sizing: border-box;
height: min(22.5rem, var(--total-size));
max-height: var(--available-height);
overflow: auto;
overscroll-behavior: contain;
scroll-padding-block: 0.25rem;
}
.VirtualizedPlaceholder {
width: 100%;
position: relative;
}
.List {
padding: 0;
}
.Item {
box-sizing: border-box;
outline: 0;
cursor: default;
-webkit-user-select: none;
user-select: none;
padding-block: 0.5rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
display: flex;
font-size: 0.875rem;
line-height: 1rem;
&[data-highlighted] {
z-index: 0;
position: relative;
color: white;
@media (prefers-color-scheme: dark) {
color: oklch(14.5% 0 0deg);
}
}
&[data-highlighted]::before {
content: '';
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0;
background-color: oklch(14.5% 0 0deg);
@media (prefers-color-scheme: dark) {
background-color: white;
}
}
}
.Empty {
box-sizing: border-box;
font-size: 0.875rem;
line-height: 1rem;
color: oklch(55.6% 0 0deg);
padding: 0.75rem 0.5rem;
@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
```
```tsx
/* index.tsx */
'use client';
import * as React from 'react';
import { Autocomplete } from '@base-ui/react/autocomplete';
import { useVirtualizer } from '@tanstack/react-virtual';
import styles from './index.module.css';
export default function ExampleVirtualizedAutocomplete() {
const virtualizerRef = React.useRef(null);
return (
{
const virtualizer = virtualizerRef.current;
if (!item || !virtualizer) {
return;
}
const isStart = index === 0;
const isEnd = index === virtualizer.options.count - 1;
const shouldScroll = reason === 'none' || (reason === 'keyboard' && (isStart || isEnd));
if (shouldScroll) {
queueMicrotask(() => {
virtualizer.scrollToIndex(index, { align: isEnd ? 'start' : 'end' });
});
}
}}
>
Search 10,000 items
No items found.
);
}
function VirtualizedList({
virtualizerRef,
}: {
virtualizerRef: React.RefObject;
}) {
const filteredItems = Autocomplete.useFilteredItems();
const scrollElementRef = React.useRef(null);
const virtualizer = useVirtualizer({
count: filteredItems.length,
getScrollElement: () => scrollElementRef.current,
estimateSize: () => 32,
overscan: 20,
paddingStart: 4,
paddingEnd: 4,
scrollPaddingEnd: 4,
scrollPaddingStart: 4,
});
React.useImperativeHandle(virtualizerRef, () => virtualizer);
const handleScrollElementRef = React.useCallback(
(element: HTMLDivElement | null) => {
scrollElementRef.current = element;
if (element) {
virtualizer.measure();
}
},
[virtualizer],
);
const totalSize = virtualizer.getTotalSize();
if (!filteredItems.length) {
return null;
}
return (
{virtualizer.getVirtualItems().map((virtualItem) => {
const item = filteredItems[virtualItem.index];
if (!item) {
return null;
}
return (
{item.name}
);
})}
);
}
interface VirtualizedItem {
id: string;
name: string;
}
function getItemLabel(item: VirtualizedItem | null) {
return item ? item.name : '';
}
const virtualizedItems: VirtualizedItem[] = Array.from({ length: 10000 }, (_, index) => {
const id = String(index + 1);
const indexLabel = id.padStart(4, '0');
return { id, name: `Item ${indexLabel}` };
});
type Virtualizer = ReturnType>;
```
#### API reference
##### Root
Groups all parts of the autocomplete.
Doesn't render its own HTML element.
**Root Props:**
| Prop | Type | Default | Description |
| :------------------- | :-------------------------------------------------------------------------------------------------------------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | `string` | - | Identifies the field when a form is submitted. |
| defaultValue | `string \| number \| string[]` | - | The uncontrolled input value of the autocomplete when it's initially rendered. To render a controlled autocomplete, use the `value` prop instead. |
| value | `string \| string[] \| number` | - | The input value of the autocomplete. Use when controlled. |
| onValueChange | `((value: string, eventDetails: Autocomplete.Root.ChangeEventDetails) => void)` | - | Event handler called when the input value of the autocomplete changes. |
| defaultOpen | `boolean` | `false` | Whether the popup is initially open. To render a controlled popup, use the `open` prop instead. |
| open | `boolean` | - | Whether the popup is currently open. Use when controlled. |
| onOpenChange | `((open: boolean, eventDetails: Autocomplete.Root.ChangeEventDetails) => void)` | - | Event handler called when the popup is opened or closed. |
| autoHighlight | `boolean \| 'always'` | `false` | Whether the first matching item is highlighted automatically. `true`: highlight after the user types and keep the highlight while the query changes.`'always'`: always highlight the first item. |
| keepHighlight | `boolean` | `false` | Whether the highlighted item should be preserved when the pointer leaves the list. |
| highlightItemOnHover | `boolean` | `true` | Whether moving the pointer over items should highlight them.
Disabling this prop allows CSS `:hover` to be differentiated from the `:focus` (`data-highlighted`) state. |
| actionsRef | `React.RefObject` | - | A ref to imperative actions. `unmount`: Manually unmounts the autocomplete.
Call this after any externally controlled closing animation finishes. |
| filter | `((itemValue: ItemValue, query: string, itemToString?: ((itemValue: ItemValue) => string)) => boolean) \| null` | - | AutocompleteFilter function used to match items vs input query. |
| filteredItems | `any[] \| Group[]` | - | Filtered items to display in the list.
When provided, the list will use these items instead of filtering the `items` prop internally.
Use when you want to control filtering logic externally with the `useFilter()` hook. |
| form | `string` | - | Identifies the form that owns the internal input.
Useful when the combobox is rendered outside the form. |
| grid | `boolean` | `false` | Whether list items are presented in a grid layout.
When enabled, arrow keys navigate across rows and columns inferred from DOM rows. |
| inline | `boolean` | `false` | Whether the list is rendered inline without using the component's own popup. Specify `open` unconditionally in conjunction with this prop so the list is considered
visible: `` |
| itemToStringValue | `((itemValue: ItemValue) => string)` | - | When the item values are objects (``), this function converts the object value to a string representation for both display in the input and form submission.
If the shape of the object is `{ value, label }`, the label will be used automatically without needing to specify this prop. |
| items | `({ items: any[] })[] \| ItemValue[]` | - | The items to be displayed in the list.
Can be either a flat array of items or an array of groups with items. |
| limit | `number` | `-1` | The maximum number of items to display in the list. |
| locale | `Intl.LocalesArgument` | - | The locale to use for string comparison.
Defaults to the user's runtime locale. |
| loopFocus | `boolean` | `true` | Whether to loop keyboard focus back to the input when the end of the list is reached while using the arrow keys. The first item can then be reached by pressing ArrowDown again from the input, or the last item can be reached by pressing ArrowUp from the input.
The input is always included in the focus loop per [ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/).
When disabled, focus does not move when on the last element and the user presses ArrowDown, or when on the first element and the user presses ArrowUp. |
| modal | `boolean` | `false` | Determines if the popup enters a modal state when open. `true`: user interaction is limited to the popup: document page scroll is locked and pointer interactions on outside elements are disabled.`false`: user interaction with the rest of the document is allowed. |
| mode | `'list' \| 'both' \| 'inline' \| 'none'` | `'list'` | Controls how the autocomplete behaves with respect to list filtering and inline autocompletion. `list` (default): items are dynamically filtered based on the input value. The input value does not change based on the active item.`both`: items are dynamically filtered based on the input value, which will temporarily change based on the active item (inline autocompletion).`inline`: items are static (not filtered), and the input value will temporarily change based on the active item (inline autocompletion).`none`: items are static (not filtered), and the input value will not change based on the active item. |
| onItemHighlighted | `((highlightedValue: ItemValue \| undefined, eventDetails: Autocomplete.Root.HighlightEventDetails) => void)` | - | Callback fired when an item is highlighted or unhighlighted.
Receives the highlighted item value (or `undefined` if no item is highlighted) and event details with a `reason` property describing why the highlight changed.
The `reason` can be: `'keyboard'`: the highlight changed due to keyboard navigation.`'pointer'`: the highlight changed due to pointer hovering.`'none'`: the highlight changed programmatically. |
| onOpenChangeComplete | `((open: boolean) => void)` | - | Event handler called after any animations complete when the popup is opened or closed. |
| openOnInputClick | `boolean` | `false` | Whether the popup opens when clicking the input. |
| submitOnItemClick | `boolean` | `false` | Whether clicking an item should submit the autocomplete's owning form.
By default, clicking an item via a pointer or Enter key does not submit the owning form.
Useful when the autocomplete is used as a single-field form search input. |
| virtualized | `boolean` | `false` | Whether the items are being externally virtualized. |
| disabled | `boolean` | `false` | Whether the component should ignore user interaction. |
| readOnly | `boolean` | `false` | Whether the user should be unable to choose a different option from the popup. |
| required | `boolean` | `false` | Whether the user must choose a value before submitting a form. |
| inputRef | `React.Ref` | - | A ref to the hidden input element. |
| id | `string` | - | The id of the component. |
| children | `React.ReactNode` | - | - |
##### Root.Props
Re-export of [Root](#autocomplete) props.
##### Root.State
```typescript
type AutocompleteRootState = {};
```
##### Root.Actions
```typescript
type AutocompleteRootActions = { unmount: () => void };
```
##### Root.ChangeEventReason
```typescript
type AutocompleteRootChangeEventReason =
| 'trigger-press'
| 'outside-press'
| 'item-press'
| 'close-press'
| 'escape-key'
| 'list-navigation'
| 'focus-out'
| 'input-change'
| 'input-clear'
| 'clear-press'
| 'chip-remove-press'
| 'none';
```
##### Root.ChangeEventDetails
```typescript
type AutocompleteRootChangeEventDetails = (
| { reason: 'none'; event: Event }
| { reason: 'trigger-press'; event: MouseEvent | PointerEvent | TouchEvent | KeyboardEvent }
| { reason: 'outside-press'; event: MouseEvent | PointerEvent | TouchEvent }
| { reason: 'item-press'; event: MouseEvent | PointerEvent | KeyboardEvent }
| { reason: 'close-press'; event: MouseEvent | PointerEvent | KeyboardEvent }
| { reason: 'escape-key'; event: KeyboardEvent }
| { reason: 'list-navigation'; event: KeyboardEvent }
| { reason: 'focus-out'; event: KeyboardEvent | FocusEvent }
| { reason: 'input-change'; event: Event | InputEvent }
| { reason: 'input-clear'; event: Event | FocusEvent | InputEvent }
| { reason: 'clear-press'; event: MouseEvent | PointerEvent | KeyboardEvent }
| { reason: 'chip-remove-press'; event: MouseEvent | PointerEvent | KeyboardEvent }
) & {
/** Cancels Base UI from handling the event. */
cancel: () => void;
/** Allows the event to propagate in cases where Base UI will stop the propagation. */
allowPropagation: () => void;
/** Indicates whether the event has been canceled. */
isCanceled: boolean;
/** Indicates whether the event is allowed to propagate. */
isPropagationAllowed: boolean;
/** The element that triggered the event, if applicable. */
trigger: Element | undefined;
};
```
##### Root.HighlightEventReason
```typescript
type AutocompleteRootHighlightEventReason = 'keyboard' | 'pointer' | 'none';
```
##### Root.HighlightEventDetails
```typescript
type AutocompleteRootHighlightEventDetails =
| { reason: 'none'; event: Event; index: number }
| { reason: 'keyboard'; event: KeyboardEvent; index: number }
| { reason: 'pointer'; event: PointerEvent; index: number };
```
##### Trigger
A button that opens the popup.
Renders a `