Built on cmdk (CommandDialog additionally wraps Dialog) — a searchable list with live fuzzy filtering, keyboard navigation, and an auto-toggling empty state, all built into cmdk itself.
npx chandra-asri-ui add commandType to filter — matching is fuzzy and handled entirely by cmdk, no filter logic written in this demo. Arrow keys move the highlight, Enter selects, and the first match auto-highlights as you type so Enter works immediately without touching the arrow keys first. CommandGroup headings and CommandEmpty both show/hide themselves automatically based on filter results.
"use client";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
} from "@/app/components/ui/command";
/**
* `Command` composed inline (not `CommandDialog`) — the pattern actually
* used on the source platform (`AddEventModal.tsx`'s category search), one
* field inside a larger form rather than a full-screen palette. Typing
* filters `CommandItem`s live via `cmdk`'s built-in fuzzy match — no filter
* logic written here — and `CommandEmpty` shows itself automatically when
* nothing matches.
*/
export function CommandDemo() {
return (
<Command className="border-border w-[320px] rounded-lg border shadow-md">
<CommandInput placeholder="Search category..." />
<CommandList>
<CommandEmpty>No category found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CommandItem>
Marketing
<CommandShortcut>⌘M</CommandShortcut>
</CommandItem>
<CommandItem>Webinar</CommandItem>
<CommandItem>Product Launch</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Other">
<CommandItem>Internal</CommandItem>
<CommandItem disabled>Archived (disabled)</CommandItem>
</CommandGroup>
</CommandList>
</Command>
);
}
Two valid ways to use it. The example above composes Command inline — the pattern actually used on the source platform, one field inside a larger form (a category-search combobox) rather than a full-screen palette. CommandDialog is the alternative: a full command-palette experience with Command pre-wired inside a Dialog, sharing all of Dialog's modal behavior (focus trap, scroll lock, Escape/overlay dismiss). Since cmdk has no title concept of its own but Radix Dialog requires one for accessibility, CommandDialog renders a visually-hidden DialogTitle automatically (override it via the title prop).
CommandShortcut is purely visual (renders text like ⌘K) — it does not bind a real keyboard shortcut. Wire that up separately with your own keydown listener on whichever component renders the trigger.
<CommandDialog open={open} onOpenChange={setOpen} title="Search">
<CommandInput placeholder="Type a command..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CommandItem>Calendar</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>"use client";
import { Dialog, DialogContent, DialogTitle } from "@/app/components/ui/dialog";
import { cn } from "@/lib/utils";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import * as React from "react";
/**
* Built on `cmdk` (not a Radix primitive directly, though `CommandDialog`
* below wraps `Dialog`, which is). Live filtering, fuzzy match, keyboard
* navigation (Arrow keys + Enter), auto-highlight of the first match,
* cross-group filtering, and the empty-state toggle are all bawaan `cmdk` —
* nothing here reimplements that logic.
*
* Two composition patterns, both valid:
* - `CommandDialog`: full command-palette experience, `Command` pre-wired
* inside a `Dialog`.
* - Compose `Command` manually inside your own `Dialog` (or inline, no
* dialog at all) when it's one field in a larger form rather than a
* full-screen palette.
*
* `CommandShortcut` is purely visual (renders text like "⌘K") — it does not
* bind a real keyboard shortcut. Wire that up yourself with a `keydown`
* listener in the component that renders the trigger.
*/
export const Command = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"bg-background text-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className,
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
/**
* Radix Dialog requires a `DialogTitle` inside every `DialogContent` for
* `aria-labelledby` (see dialog.tsx) — `cmdk` has no title concept of its
* own, so this renders one visually hidden by default rather than skip it.
*/
export function CommandDialog({
children,
title = "Command Palette",
...props
}: React.ComponentPropsWithoutRef<typeof Dialog> & { title?: string }) {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0">
<DialogTitle className="sr-only">{title}</DialogTitle>
<Command className="[&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
}
export const CommandInput = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="border-border flex items-center border-b px-3">
<Search className="text-muted mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"placeholder:text-muted flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
export const CommandList = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-x-hidden overflow-y-auto", className)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
export const CommandEmpty = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
export const CommandGroup = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"text-foreground overflow-hidden p-1",
"[&_[cmdk-group-heading]]:text-muted [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className,
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
export const CommandSeparator = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
export const CommandItem = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none",
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground",
"data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50",
className,
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
export function CommandShortcut({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) {
return (
<span
className={cn("text-muted ml-auto text-xs tracking-widest", className)}
{...props}
/>
);
}
CommandShortcut.displayName = "CommandShortcut";