Overlays
Command Menu
A ⌘K command palette built on react-aria-components' Autocomplete and Modal, styled with the quebi design system. Renders a dark surface inside a dimmed overlay with a quebi search field, sectioned results, keyboard shortcut hints, and a footer. Composes the menu and dropdown item styling for brand-teal selection.
overlaycommandpalettecmdksearchmenukeyboard
Command palette
Open with the button or press ⌘K. Sectioned results with icons, descriptions, and shortcut hints.
Pending state
Set `isPending` to swap the search icon for a spinner while results load.
Source
Copy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.
"use client"
import { Search } from "lucide-react"
import { createContext, use, useEffect } from "react"
import type {
AutocompleteProps,
CollectionRenderer,
MenuProps,
MenuTriggerProps,
ModalOverlayProps,
SearchFieldProps,
} from "react-aria-components"
import {
Autocomplete,
Button,
Collection,
CollectionRendererContext,
DefaultCollectionRenderer,
Dialog,
Header,
Input,
Menu as MenuPrimitive,
MenuSection,
Modal,
ModalContext,
ModalOverlay,
OverlayTriggerStateContext,
SearchField,
useFilter,
} from "react-aria-components"
import { DropdownKeyboard } from "@/components/dropdown"
import { Loader } from "@/components/loader"
import {
MenuDescription,
MenuItem,
MenuLabel,
type MenuSectionProps,
MenuSeparator,
} from "@/components/menu"
import { cn } from "@/lib/utils"
/**
* Command Menu — quebi design system
*
* A ⌘K command palette built on react-aria-components' Autocomplete + Modal.
* Renders a dark quebi surface inside a dimmed overlay, with a quebi-styled
* search field, sectioned results, keyboard shortcut hints, and a footer.
* Selected/focused items reuse the shared Menu/Dropdown styling (brand-teal
* selection, subtle white hover wash on the dark surface).
*/
interface CommandMenuProviderProps {
isPending?: boolean
escapeButton?: boolean
}
const CommandMenuContext = createContext<CommandMenuProviderProps | undefined>(undefined)
const useCommandMenu = () => {
const context = use(CommandMenuContext)
if (!context) {
throw new Error("useCommandMenu must be used within a <CommandMenuProvider />")
}
return context
}
const sizes = {
xs: "sm:max-w-xs",
sm: "sm:max-w-sm",
md: "sm:max-w-md",
lg: "sm:max-w-lg",
xl: "sm:max-w-xl",
"2xl": "sm:max-w-2xl",
"3xl": "sm:max-w-3xl",
}
interface CommandMenuProps extends AutocompleteProps, MenuTriggerProps, CommandMenuProviderProps {
isDismissable?: boolean
"aria-label"?: string
shortcut?: string
className?: string
size?: keyof typeof sizes
overlay?: Pick<ModalOverlayProps, "className">
}
const CommandMenu = ({
onOpenChange,
className,
isDismissable = true,
escapeButton = true,
isPending,
overlay,
size = "lg",
shortcut,
...props
}: CommandMenuProps) => {
const { contains } = useFilter({ sensitivity: "base" })
const filter = (textValue: string, inputValue: string) => contains(textValue, inputValue)
useEffect(() => {
if (!shortcut) return
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === shortcut && (e.metaKey || e.ctrlKey)) {
onOpenChange?.(true)
}
}
document.addEventListener("keydown", onKeyDown)
return () => document.removeEventListener("keydown", onKeyDown)
}, [shortcut, onOpenChange])
return (
<CommandMenuContext value={{ isPending: isPending, escapeButton: escapeButton }}>
<ModalContext value={{ isOpen: props.isOpen, onOpenChange: onOpenChange }}>
<ModalOverlay
{...props}
isDismissable={isDismissable}
className={cn(
"fixed inset-0 z-50 h-(--visual-viewport-height,100vh) w-screen overflow-hidden bg-black/60 backdrop-blur-sm",
"grid grid-rows-[1fr_auto] justify-items-center text-center sm:grid-rows-[1fr_auto_3fr]",
"entering:fade-in entering:animate-in entering:duration-300 entering:ease-out",
"exiting:fade-out exiting:animate-out exiting:ease-in",
overlay?.className,
)}
>
<Modal
className={cn(
"row-start-2 bg-quebi-bg text-start text-white shadow-quebi-glow-strong outline-none ring-1 ring-cyan-500/10 md:row-start-1",
"max-h-[calc(var(--visual-viewport-height)*0.8)] w-full sm:fixed sm:top-[10%] sm:left-1/2 sm:-translate-x-1/2",
"rounded-t-quebi-md md:rounded-quebi-md",
sizes[size],
"entering:slide-in-from-bottom sm:entering:zoom-in-95 sm:entering:slide-in-from-bottom-0 entering:animate-in entering:duration-300 entering:ease-out",
"exiting:slide-out-to-bottom sm:exiting:zoom-out-95 sm:exiting:slide-out-to-bottom-0 exiting:animate-out exiting:ease-in",
className,
)}
>
<Dialog
aria-label={props["aria-label"] ?? "Command Menu"}
className="flex max-h-[inherit] flex-col overflow-hidden outline-hidden"
>
<Autocomplete filter={filter} {...props} />
</Dialog>
</Modal>
</ModalOverlay>
</ModalContext>
</CommandMenuContext>
)
}
interface CommandMenuSearchProps extends SearchFieldProps {
placeholder?: string
className?: string
}
const CommandMenuSearch = ({ className, placeholder, ...props }: CommandMenuSearchProps) => {
const state = use(OverlayTriggerStateContext)
if (!state) throw new Error("CommandMenuSearch must be used within a CommandMenu")
const { isPending, escapeButton } = useCommandMenu()
return (
<SearchField
aria-label="Quick search"
autoFocus
className={cn("flex w-full items-center px-2.5 py-1", className)}
{...props}
>
{isPending ? (
<Loader className="size-4.5" variant="spin" />
) : (
<Search
data-slot="command-menu-search-icon"
className="size-5 shrink-0 text-quebi-fg-muted"
/>
)}
<Input
placeholder={placeholder ?? "Search..."}
className="w-full min-w-0 bg-transparent px-2.5 py-2 text-base text-white placeholder-quebi-fg-subtle outline-hidden focus:outline-hidden sm:px-2 sm:py-1.5 sm:text-sm [&::-ms-reveal]:hidden [&::-webkit-search-cancel-button]:hidden"
/>
{escapeButton && (
<Button
onPress={() => state?.close()}
className="hidden cursor-default rounded-quebi-sm border border-cyan-500/20 text-quebi-fg-muted hover:bg-white/[0.04] hover:text-white lg:inline lg:px-1.5 lg:py-0.5 lg:text-xs"
>
Esc
</Button>
)}
</SearchField>
)
}
const CommandMenuList = <T extends object>({ className, ...props }: MenuProps<T>) => {
return (
<CollectionRendererContext.Provider value={renderer}>
<MenuPrimitive
className={cn(
"grid max-h-full flex-1 grid-cols-[auto_1fr] content-start overflow-y-auto border-cyan-500/10 border-t p-2 sm:max-h-110 *:[[role=group]]:mb-6 *:[[role=group]]:last:mb-0",
className,
)}
{...props}
/>
</CollectionRendererContext.Provider>
)
}
const CommandMenuSection = <T extends object>({
className,
ref,
...props
}: MenuSectionProps<T>) => {
return (
<MenuSection
ref={ref}
className={cn("col-span-full grid grid-cols-[auto_1fr] content-start gap-y-0.25", className)}
{...props}
>
{"label" in props && (
<Header className="col-span-full mb-1 block min-w-(--trigger-width) truncate px-2.5 text-quebi-fg-muted text-xs">
{props.label}
</Header>
)}
<Collection items={props.items}>{props.children}</Collection>
</MenuSection>
)
}
const CommandMenuItem = ({ className, ...props }: React.ComponentProps<typeof MenuItem>) => {
const textValue =
props.textValue || (typeof props.children === "string" ? props.children : undefined)
return (
<MenuItem
{...props}
textValue={textValue}
className={cn("items-center gap-y-0.5", className)}
/>
)
}
interface CommandMenuDescriptionProps extends React.ComponentProps<typeof MenuDescription> {}
const CommandMenuDescription = ({ className, ...props }: CommandMenuDescriptionProps) => {
return (
<MenuDescription className={cn("col-start-3 row-start-1 ms-auto", className)} {...props} />
)
}
const renderer: CollectionRenderer = {
CollectionRoot(props) {
if (props.collection.size === 0) {
return (
<div className="col-span-full p-4 text-center text-quebi-fg-muted text-sm">
No results found.
</div>
)
}
return <DefaultCollectionRenderer.CollectionRoot {...props} />
},
CollectionBranch: DefaultCollectionRenderer.CollectionBranch,
}
const CommandMenuSeparator = ({
className,
...props
}: React.ComponentProps<typeof MenuSeparator>) => (
<MenuSeparator className={cn("-mx-2", className)} {...props} />
)
const CommandMenuFooter = ({ className, ...props }: React.ComponentProps<"div">) => {
return (
<div
className={cn(
"col-span-full flex-none border-cyan-500/10 border-t px-2 py-1.5 text-quebi-fg-muted text-sm",
"*:[kbd]:inset-ring *:[kbd]:inset-ring-white/10 *:[kbd]:mx-1 *:[kbd]:inline-grid *:[kbd]:h-4 *:[kbd]:min-w-4 *:[kbd]:place-content-center *:[kbd]:rounded-quebi-sm *:[kbd]:bg-white/[0.04]",
className,
)}
{...props}
/>
)
}
const CommandMenuLabel = MenuLabel
const CommandMenuShortcut = ({
className,
...props
}: React.ComponentProps<typeof DropdownKeyboard>) => (
<DropdownKeyboard
className={cn(
"gap-0.5 font-sans text-[10.5px] uppercase *:inset-ring *:inset-ring-white/20 *:grid *:size-5.5 *:place-content-center *:rounded-quebi-sm *:bg-quebi-bg",
className,
)}
{...props}
/>
)
export type { CommandMenuDescriptionProps, CommandMenuProps, CommandMenuSearchProps }
export {
CommandMenu,
CommandMenuDescription,
CommandMenuFooter,
CommandMenuItem,
CommandMenuLabel,
CommandMenuList,
CommandMenuSearch,
CommandMenuSection,
CommandMenuSeparator,
CommandMenuShortcut,
}