Command Palette
A reusable command dialog wrapper for grouped actions, app navigation, and searchable command menus.
registry/corr/components/command-palette.tsx
"use client"
import * as React from "react"
import { SearchIcon } from "lucide-react"
import { AnimatePresence, motion, useReducedMotion } from "motion/react"
import { Button } from "@/components/ui/button"
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
export type CommandPaletteItem = {
id: string
label: string
description?: string
icon?: React.ReactNode
keywords?: string[]
onSelect?: () => void
}
export type CommandPaletteGroup = {
id: string
label: string
items: CommandPaletteItem[]
}
export function CommandPalette({
open,
onOpenChange,
groups,
placeholder = "Type a command or search...",
emptyLabel = "No results found.",
trigger,
}: {
open?: boolean
onOpenChange?: (open: boolean) => void
groups: CommandPaletteGroup[]
placeholder?: string
emptyLabel?: string
trigger?: React.ReactNode
}) {
const [internalOpen, setInternalOpen] = React.useState(false)
const isControlled = typeof open === "boolean"
const currentOpen = isControlled ? open : internalOpen
const reduceMotion = useReducedMotion()
function setOpen(nextOpen: boolean) {
if (!isControlled) setInternalOpen(nextOpen)
onOpenChange?.(nextOpen)
}
return (
<>
{trigger ? (
<span onClick={() => setOpen(true)}>{trigger}</span>
) : (
<Button type="button" variant="outline" onClick={() => setOpen(true)}>
<SearchIcon className="size-4" />
Open command palette
</Button>
)}
<CommandDialog open={currentOpen} onOpenChange={setOpen}>
<Command>
<CommandInput placeholder={placeholder} />
<CommandList>
<CommandEmpty>{emptyLabel}</CommandEmpty>
{groups.map((group) => (
<CommandGroup key={group.id} heading={group.label}>
<AnimatePresence initial={false}>
{group.items.map((item, index) => (
<motion.div
key={item.id}
initial={{
opacity: 0,
y: reduceMotion ? 0 : 4,
}}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{
duration: reduceMotion ? 0 : 0.14,
delay: reduceMotion ? 0 : index * 0.015,
}}
>
<CommandItem
value={[
item.label,
item.description,
...(item.keywords ?? []),
]
.filter(Boolean)
.join(" ")}
onSelect={() => {
item.onSelect?.()
setOpen(false)
}}
>
{item.icon}
<span className="min-w-0">
<span className="block truncate">{item.label}</span>
{item.description ? (
<span className="block truncate text-xs text-muted-foreground">
{item.description}
</span>
) : null}
</span>
</CommandItem>
</motion.div>
))}
</AnimatePresence>
</CommandGroup>
))}
</CommandList>
</Command>
</CommandDialog>
</>
)
}
- Interaction
- modaldropdown
DependenciesBundle weight · medium
- icons
- lucide-react
- animation
- motion