Composable command menu.
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandShortcut, } from "@repo/tailwindcss/ui/command"; import { For, type JSXElement } from "solid-js"; type Option = { icon: JSXElement; label: string; disabled: boolean; shortcut?: JSXElement; }; type List = { label: string; options: Option[]; }; export const commandData: List[] = [ { label: "Suggestions", options: [ { icon: ( <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="mr-2 h-4 w-4" > <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2zm12-4v4M8 3v4m-4 4h16m-9 4h1m0 0v3" /> </svg> ), label: "Calendar", disabled: false, }, { icon: ( <svg xmlns="http://www.w3.org/2000/svg" class="mr-2 h-4 w-4" viewBox="0 0 24 24" > <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" > <path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0-18 0m6-2h.01M15 10h.01" /> <path d="M9.5 15a3.5 3.5 0 0 0 5 0" /> </g> </svg> ), label: "Search emoji", disabled: false, }, { icon: ( <svg xmlns="http://www.w3.org/2000/svg" class="mr-2 h-4 w-4" viewBox="0 0 24 24" > <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" > <path d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3-5a9 9 0 0 0 6-8a3 3 0 0 0-3-3a9 9 0 0 0-8 6a6 6 0 0 0-5 3" /> <path d="M7 14a6 6 0 0 0-3 6a6 6 0 0 0 6-3m4-8a1 1 0 1 0 2 0a1 1 0 1 0-2 0" /> </g> </svg> ), label: "Launch", disabled: false, }, ], }, { label: "Settings", options: [ { icon: ( <svg xmlns="http://www.w3.org/2000/svg" class="mr-2 h-4 w-4" viewBox="0 0 24 24" > <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0-8 0M6 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2" /> </svg> ), label: "Profile", disabled: true, shortcut: <CommandShortcut>⌘P</CommandShortcut>, }, { icon: ( <svg xmlns="http://www.w3.org/2000/svg" class="mr-2 h-4 w-4" viewBox="0 0 24 24" > <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" > <path d="M3 7a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /> <path d="m3 7l9 6l9-6" /> </g> </svg> ), label: "Mail", disabled: false, shortcut: <CommandShortcut>⌘B</CommandShortcut>, }, { icon: ( <svg xmlns="http://www.w3.org/2000/svg" class="mr-2 h-4 w-4" viewBox="0 0 24 24" > <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" > <path d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 0 0-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 0 0-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 0 0-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 0 0-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 0 0 1.066-2.573c-.94-1.543.826-3.31 2.37-2.37c1 .608 2.296.07 2.572-1.065" /> <path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0-6 0" /> </g> </svg> ), label: "Setting", disabled: false, shortcut: <CommandShortcut>⌘S</CommandShortcut>, }, ], }, ]; const CommandDemo = () => { return ( <Command class="rounded-lg border shadow-md"> <CommandInput placeholder="Type a command or search..." /> <CommandList> <CommandEmpty>No results found.</CommandEmpty> <For each={commandData}> {(item) => ( <CommandGroup heading={item.label}> <For each={item.options}> {(item) => ( <CommandItem disabled={item.disabled}> {item.icon} <span>{item.label}</span> {item.shortcut} </CommandItem> )} </For> </CommandGroup> )} </For> </CommandList> </Command> ); }; export default CommandDemo;
npx shadcn-solid@latest add command
npm install cmdk-solid
import { cn } from "@/libs/cn"; import type { CommandDialogProps, CommandEmptyProps, CommandGroupProps, CommandInputProps, CommandItemProps, CommandListProps, CommandRootProps, } from "cmdk-solid"; import { Command as CommandPrimitive } from "cmdk-solid"; import type { ComponentProps, VoidProps } from "solid-js"; import { splitProps } from "solid-js"; import { Dialog, DialogContent } from "./dialog"; export const Command = (props: CommandRootProps) => { const [local, rest] = splitProps(props, ["class"]); return ( <CommandPrimitive class={cn( "flex size-full flex-col overflow-hidden bg-popover text-popover-foreground", local.class, )} {...rest} /> ); }; export const CommandList = (props: CommandListProps) => { const [local, rest] = splitProps(props, ["class"]); return ( <CommandPrimitive.List class={cn( "max-h-[300px] overflow-y-auto overflow-x-hidden p-1", local.class, )} {...rest} /> ); }; export const CommandInput = (props: VoidProps<CommandInputProps>) => { const [local, rest] = splitProps(props, ["class"]); return ( <div class="flex items-center border-b px-3" cmdk-input-wrapper=""> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="mr-2 h-4 w-4 shrink-0 opacity-50" > <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10a7 7 0 1 0 14 0a7 7 0 1 0-14 0m18 11l-6-6" /> <title>Search</title> </svg> <CommandPrimitive.Input class={cn( "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", local.class, )} {...rest} /> </div> ); }; export const CommandItem = (props: CommandItemProps) => { const [local, rest] = splitProps(props, ["class"]); return ( <CommandPrimitive.Item class={cn( "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-selected:bg-accent aria-selected:text-accent-foreground", local.class, )} {...rest} /> ); }; export const CommandShortcut = (props: ComponentProps<"span">) => { const [local, rest] = splitProps(props, ["class"]); return ( <span class={cn( "ml-auto text-xs tracking-widest text-muted-foreground", local.class, )} {...rest} /> ); }; export const CommandDialog = (props: CommandDialogProps) => { const [local, rest] = splitProps(props, ["children"]); return ( <Dialog {...rest}> <DialogContent class="overflow-hidden p-0"> <Command class="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:size-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:size-5"> {local.children} </Command> </DialogContent> </Dialog> ); }; export const CommandEmpty = (props: CommandEmptyProps) => { const [local, rest] = splitProps(props, ["class"]); return ( <CommandPrimitive.Empty class={cn("py-6 text-center text-sm", local.class)} {...rest} /> ); }; export const CommandGroup = (props: CommandGroupProps) => { const [local, rest] = splitProps(props, ["class"]); return ( <CommandPrimitive.Group class={cn( "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground", local.class, )} {...rest} /> ); }; export const CommandSeparator = (props: CommandEmptyProps) => { const [local, rest] = splitProps(props, ["class"]); return ( <CommandPrimitive.Separator class={cn("-mx-1 h-px bg-border", local.class)} {...rest} /> ); };
import { cn } from "@/libs/cn"; import type { CommandDialogProps, CommandEmptyProps, CommandGroupProps, CommandInputProps, CommandItemProps, CommandListProps, CommandRootProps, } from "cmdk-solid"; import { Command as CommandPrimitive } from "cmdk-solid"; import type { ComponentProps, VoidProps } from "solid-js"; import { splitProps } from "solid-js"; import { Dialog, DialogContent } from "./dialog"; export const Command = (props: CommandRootProps) => { const [local, rest] = splitProps(props, ["class"]); return ( <CommandPrimitive class={cn( "flex size-full flex-col overflow-hidden bg-popover text-popover-foreground", local.class, )} {...rest} /> ); }; export const CommandList = (props: CommandListProps) => { const [local, rest] = splitProps(props, ["class"]); return ( <CommandPrimitive.List class={cn( "max-h-[300px] overflow-y-auto overflow-x-hidden p-1", local.class, )} {...rest} /> ); }; export const CommandInput = (props: VoidProps<CommandInputProps>) => { const [local, rest] = splitProps(props, ["class"]); return ( <div class="flex items-center border-b px-3" cmdk-input-wrapper=""> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="mr-2 h-4 w-4 shrink-0 opacity-50" > <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10a7 7 0 1 0 14 0a7 7 0 1 0-14 0m18 11l-6-6" /> <title>Search</title> </svg> <CommandPrimitive.Input class={cn( "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:(cursor-not-allowed opacity-50)", local.class, )} {...rest} /> </div> ); }; export const CommandItem = (props: CommandItemProps) => { const [local, rest] = splitProps(props, ["class"]); return ( <CommandPrimitive.Item class={cn( "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:(bg-accent text-accent-foreground) aria-disabled:(pointer-events-none opacity-50)", local.class, )} {...rest} /> ); }; export const CommandShortcut = (props: ComponentProps<"span">) => { const [local, rest] = splitProps(props, ["class"]); return ( <span class={cn( "ml-auto text-xs tracking-widest text-muted-foreground", local.class, )} {...rest} /> ); }; export const CommandDialog = (props: CommandDialogProps) => { const [local, rest] = splitProps(props, ["children"]); return ( <Dialog {...rest}> <DialogContent class="overflow-hidden p-0"> <Command class="[&_[cmdk-group-heading]]:(px-2 font-medium text-muted-foreground) [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:size-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:(px-2 py-3) [&_[cmdk-item]_svg]:size-5"> {local.children} </Command> </DialogContent> </Dialog> ); }; export const CommandEmpty = (props: CommandEmptyProps) => { const [local, rest] = splitProps(props, ["class"]); return ( <CommandPrimitive.Empty class={cn("py-6 text-center text-sm", local.class)} {...rest} /> ); }; export const CommandGroup = (props: CommandGroupProps) => { const [local, rest] = splitProps(props, ["class"]); return ( <CommandPrimitive.Group class={cn( "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:(px-2 py-1.5 text-xs font-medium text-muted-foreground)", local.class, )} {...rest} /> ); }; export const CommandSeparator = (props: CommandEmptyProps) => { const [local, rest] = splitProps(props, ["class"]); return ( <CommandPrimitive.Separator class={cn("-mx-1 h-px bg-border", local.class)} {...rest} /> ); };
import { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, } from "@/components/ui/command"
<Command> <CommandInput placeholder="Type a command or search..." /> <CommandList> <CommandEmpty>No results found.</CommandEmpty> <CommandGroup heading="Suggestions"> <CommandItem>Calendar</CommandItem> <CommandItem>Search Emoji</CommandItem> <CommandItem>Calculator</CommandItem> </CommandGroup> <CommandSeparator /> <CommandGroup heading="Settings"> <CommandItem>Profile</CommandItem> <CommandItem>Billing</CommandItem> <CommandItem>Settings</CommandItem> </CommandGroup> </CommandList> </Command>
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@repo/tailwindcss/ui/command"; import { For, createEffect, createSignal, onCleanup } from "solid-js"; import { commandData } from "./command-demo"; const CommandDialogDemo = () => { const [open, setOpen] = createSignal(false); createEffect(() => { const down = (e: KeyboardEvent) => { if (e.key === "j" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); setOpen((open) => !open); } }; document.addEventListener("keydown", down); onCleanup(() => { document.removeEventListener("keydown", down); }); }); return ( <> <p class="text-sm text-muted-foreground"> Press{" "} <kbd class="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100"> <span class="text-xs">⌘</span>J </kbd> </p> <CommandDialog class="rounded-lg border shadow-md" open={open()} onOpenChange={setOpen} > <CommandInput placeholder="Type a command or search..." /> <CommandList> <CommandEmpty>No results found.</CommandEmpty> <For each={commandData}> {(item) => ( <CommandGroup heading={item.label}> <For each={item.options}> {(item) => ( <CommandItem disabled={item.disabled}> {item.icon} <span>{item.label}</span> {item.shortcut} </CommandItem> )} </For> </CommandGroup> )} </For> </CommandList> </CommandDialog> </> ); }; export default CommandDialogDemo;
To show the command menu in a dialog, use the <CommandDialog /> component.
<CommandDialog />
export function CommandMenu() { const [open, setOpen] = createSignal(false); createEffect(() => { const down = (e: KeyboardEvent) => { if (e.key === "j" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); setOpen(open => !open); } }; document.addEventListener("keydown", down); onCleanup(() => { document.removeEventListener("keydown", down); }); }); return ( <CommandDialog open={open()} onOpenChange={setOpen}> <CommandInput placeholder="Type a command or search..." /> <CommandList> <CommandEmpty>No results found.</CommandEmpty> <CommandGroup heading="Suggestions"> <CommandItem>Calendar</CommandItem> <CommandItem>Search Emoji</CommandItem> <CommandItem>Calculator</CommandItem> </CommandGroup> </CommandList> </CommandDialog> ); }