Command Menu with Tabs
Combining Dialog, Tab, and Combobox from Ariakit React to build a command palette component.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
import groupBy from "lodash-es/groupBy.js";import { matchSorter } from "match-sorter";import { useId, useMemo, useState } from "react";import {CommandMenu,CommandMenuFooter,CommandMenuGrid,CommandMenuGroup,CommandMenuInput,CommandMenuItem,CommandMenuList,CommandMenuTab,CommandMenuTabList,CommandMenuTabPanel,} from "./command-menu.tsx";import { flatPages, pages } from "./pages.ts";import "./theme.css";export default function Example() {return (<div className="flex gap-2 justify-center flex-wrap"><Simple /><WithTabs cols={1} /><WithTabs cols={2} /><WithTabs cols={3} /></div>);}const categories = ["All", ...Object.keys(pages)];function getCategoryId(category: string, prefix: string) {return `${prefix}/${category}`;}function getCategoryLabel(tabId: string, prefix: string) {return tabId.replace(`${prefix}/`, "");}function Simple() {const [open, setOpen] = useState(false);const [searchValue, setSearchValue] = useState("");const matches = useMemo(() => {if (!searchValue) return flatPages;return matchSorter(flatPages, searchValue, { keys: ["label", "path"] });}, [searchValue]);const currentPages = matches;return (<>className="ak-button ak-button-default"onClick={() => setOpen(true)}>Simple<CommandMenuaria-label="Command Menu"open={open}setOpen={setOpen}onSearch={setSearchValue}><CommandMenuInput placeholder="Search pages..." /><CommandMenuList>{currentPages.map((page) => (<CommandMenuItem key={page.label} render={<a href={page.path} />}><span className="truncate">{page.label}</span></CommandMenuItem>))}</CommandMenuList><CommandMenuFooter /></CommandMenu></>);}function WithTabs({ cols = 1 }: { cols?: number }) {const prefix = useId();const allLabel = "All";const allId = getCategoryId(allLabel, prefix);const [open, setOpen] = useState(false);const [searchValue, setSearchValue] = useState("");const [tabId, setTabId] = useState(allId);const isAllTab = tabId === allId;const [allMatches, groupMatches] = useMemo(() => {const allMatches = searchValue? matchSorter(flatPages, searchValue, { keys: ["label", "path"] }): flatPages;const groupMatches = groupBy(allMatches, "category");return [allMatches, groupMatches];}, [searchValue]);const matchedPages = isAllTab? groupMatches: groupMatches[getCategoryLabel(tabId, prefix)] || [];const renderGrid = (pages: typeof matchedPages) => {if (!Array.isArray(pages)) {return Object.entries(pages).map(([category, pages], index) => (<CommandMenuGroup key={index} label={category}>{renderGrid(pages)}</CommandMenuGroup>));}return (<CommandMenuGrid cols={cols}>{pages.map((page, index) => (<CommandMenuItemkey={page.label}index={index}render={<a href={page.path} />}><span className="truncate">{page.label}</span></CommandMenuItem>))}</CommandMenuGrid>);};return (<>className="ak-button ak-button-default"onClick={() => setOpen(true)}>With Tabs{cols > 1 ? ` (${cols} columns)` : ""}<CommandMenuaria-label="Command Menu"open={open}setOpen={setOpen}onSearch={setSearchValue}defaultTab={tabId}onTabChange={setTabId}><CommandMenuInput placeholder="Search pages..." /><CommandMenuTabList aria-label="Categories">{categories.map((label) => {const pages = label === allLabel ? allMatches : groupMatches[label];return (<CommandMenuTabkey={label}id={getCategoryId(label, prefix)}disabled={!pages?.length}>{label} ({pages?.length || 0})</CommandMenuTab>);})}</CommandMenuTabList><CommandMenuTabPanel><CommandMenuList className="flex flex-col gap-4">{renderGrid(matchedPages)}</CommandMenuList></CommandMenuTabPanel><CommandMenuFooter /></CommandMenu></>);}import groupBy from "lodash-es/groupBy.js";import { matchSorter } from "match-sorter";import { useId, useMemo, useState } from "react";import {CommandMenu,CommandMenuFooter,CommandMenuGrid,CommandMenuGroup,CommandMenuInput,CommandMenuItem,CommandMenuList,CommandMenuTab,CommandMenuTabList,CommandMenuTabPanel,} from "./command-menu.tsx";import { flatPages, pages } from "./pages.ts";import "./theme.css";export default function Example() {return (<div className="flex gap-2 justify-center flex-wrap"><Simple /><WithTabs cols={1} /><WithTabs cols={2} /><WithTabs cols={3} /></div>);}const categories = ["All", ...Object.keys(pages)];function getCategoryId(category: string, prefix: string) {return `${prefix}/${category}`;}function getCategoryLabel(tabId: string, prefix: string) {return tabId.replace(`${prefix}/`, "");}function Simple() {const [open, setOpen] = useState(false);const [searchValue, setSearchValue] = useState("");const matches = useMemo(() => {if (!searchValue) return flatPages;return matchSorter(flatPages, searchValue, { keys: ["label", "path"] });}, [searchValue]);const currentPages = matches;return (<>className="ak-button ak-button-default"onClick={() => setOpen(true)}>Simple<CommandMenuaria-label="Command Menu"open={open}setOpen={setOpen}onSearch={setSearchValue}><CommandMenuInput placeholder="Search pages..." /><CommandMenuList>{currentPages.map((page) => (<CommandMenuItem key={page.label} render={<a href={page.path} />}><span className="truncate">{page.label}</span></CommandMenuItem>))}</CommandMenuList><CommandMenuFooter /></CommandMenu></>);}function WithTabs({ cols = 1 }: { cols?: number }) {const prefix = useId();const allLabel = "All";const allId = getCategoryId(allLabel, prefix);const [open, setOpen] = useState(false);const [searchValue, setSearchValue] = useState("");const [tabId, setTabId] = useState(allId);const isAllTab = tabId === allId;const [allMatches, groupMatches] = useMemo(() => {const allMatches = searchValue? matchSorter(flatPages, searchValue, { keys: ["label", "path"] }): flatPages;const groupMatches = groupBy(allMatches, "category");return [allMatches, groupMatches];}, [searchValue]);const matchedPages = isAllTab? groupMatches: groupMatches[getCategoryLabel(tabId, prefix)] || [];const renderGrid = (pages: typeof matchedPages) => {if (!Array.isArray(pages)) {return Object.entries(pages).map(([category, pages], index) => (<CommandMenuGroup key={index} label={category}>{renderGrid(pages)}</CommandMenuGroup>));}return (<CommandMenuGrid cols={cols}>{pages.map((page, index) => (<CommandMenuItemkey={page.label}index={index}render={<a href={page.path} />}><span className="truncate">{page.label}</span></CommandMenuItem>))}</CommandMenuGrid>);};return (<>className="ak-button ak-button-default"onClick={() => setOpen(true)}>With Tabs{cols > 1 ? ` (${cols} columns)` : ""}<CommandMenuaria-label="Command Menu"open={open}setOpen={setOpen}onSearch={setSearchValue}defaultTab={tabId}onTabChange={setTabId}><CommandMenuInput placeholder="Search pages..." /><CommandMenuTabList aria-label="Categories">{categories.map((label) => {const pages = label === allLabel ? allMatches : groupMatches[label];return (<CommandMenuTabkey={label}id={getCategoryId(label, prefix)}disabled={!pages?.length}>{label} ({pages?.length || 0})</CommandMenuTab>);})}</CommandMenuTabList><CommandMenuTabPanel><CommandMenuList className="flex flex-col gap-4">{renderGrid(matchedPages)}</CommandMenuList></CommandMenuTabPanel><CommandMenuFooter /></CommandMenu></>);}
Related examples


Command MenuCombining Dialog and Combobox to enable users to search a command list in a Raycast-style modal.


Combobox with linksUsing a Combobox with items rendered as links that can be clicked with keyboard and mouse. This is useful for creating an accessible page search input in React.


Combobox filteringListing suggestions in a Combobox component based on the input value using React.startTransition to ensure the UI remains responsive during typing.


Combobox with TabsOrganizing Combobox with Tab components that support mouse, keyboard, and screen reader interactions. The UI remains responsive by using React.startTransition.


Dialog with MenuShowing a nested dropdown Menu component inside a modal Dialog using React.


Nested DialogRendering a modal Dialog to confirm an action inside another modal dialog using React.
Menu with ComboboxCombining Menu and Combobox to create a dropdown menu with a search field that can be used to filter menu items.
Select with ComboboxCombining Select and Combobox to create a dropdown with a search field that can be used to filter items.