Command
A shadcn-style command component built with Ark UI primitives.
Suggestions
Linear⌘L
Figma⌘F
Slack⌘S
Commands
Clipboard history⌘⇧C
Create snippet⌘N
System preferences⌘,
Navigate
Open
EscClose
import { ArrowDownIcon, ArrowUpIcon, CornerDownLeftIcon } from "lucide-react";
import { Fragment, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Command,
CommandDialog,
CommandDialogPopup,
CommandDialogTrigger,
CommandEmpty,
CommandFooter,
CommandGroup,
CommandGroupedList,
CommandGroupLabel,
CommandInput,
CommandItem,
CommandItemText,
CommandPopup,
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command";
type CommandPaletteItem = {
label: string;
value: string;
shortcut?: string;
group: "Suggestions" | "Commands";
};
const COMMAND_PALETTE_ITEMS: CommandPaletteItem[] = [
{ group: "Suggestions", label: "Linear", shortcut: "⌘L", value: "linear" },
{ group: "Suggestions", label: "Figma", shortcut: "⌘F", value: "figma" },
{ group: "Suggestions", label: "Slack", shortcut: "⌘S", value: "slack" },
{
group: "Commands",
label: "Clipboard history",
shortcut: "⌘⇧C",
value: "clipboard",
},
{
group: "Commands",
label: "Create snippet",
shortcut: "⌘N",
value: "snippet",
},
{
group: "Commands",
label: "System preferences",
shortcut: "⌘,",
value: "prefs",
},
];
const CommandPaletteDemo = () => {
const [open, setOpen] = useState(false);
const [resetKey, setResetKey] = useState(0);
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "j" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((o) => !o);
}
};
document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down);
}, []);
return (
<CommandDialog
onOpenChange={(d) => {
setOpen(d.open);
if (d.open) setResetKey((k) => k + 1);
}}
open={open}
>
<CommandDialogTrigger asChild>
<Button variant="outline">
Open command palette
<span className="ms-1 hidden text-muted-foreground text-xs sm:inline">
⌘J
</span>
</Button>
</CommandDialogTrigger>
<CommandDialogPopup aria-label="Command palette">
<Command
key={resetKey}
groupBy={(item) => item.group}
groupSort={["Suggestions", "Commands"]}
items={COMMAND_PALETTE_ITEMS}
open={open}
onOpenChange={(d) => {
if (!d.open) setOpen(false);
}}
onValueChange={() => {
setOpen(false);
}}
>
<CommandInput placeholder="Search for apps and commands…" />
<CommandPopup>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroupedList items={COMMAND_PALETTE_ITEMS}>
{([group, groupItems]) => (
<Fragment key={group}>
<CommandGroup>
<CommandGroupLabel>{group}</CommandGroupLabel>
{groupItems.map((item) => (
<CommandItem key={item.value} item={item}>
<CommandItemText>{item.label}</CommandItemText>
{item.shortcut ? (
<CommandShortcut>{item.shortcut}</CommandShortcut>
) : null}
</CommandItem>
))}
</CommandGroup>
{group !== "Commands" ? <CommandSeparator /> : null}
</Fragment>
)}
</CommandGroupedList>
</CommandPopup>
<CommandFooter>
<div className="flex flex-wrap items-center gap-4">
<div className="flex items-center gap-1.5">
<kbd className="pointer-events-none inline-flex h-5 items-center justify-center rounded border border-border bg-muted px-1 font-medium font-mono text-muted-foreground text-xs shadow-xs">
<ArrowUpIcon className="size-2.5 opacity-80" />
</kbd>
<kbd className="pointer-events-none inline-flex h-5 items-center justify-center rounded border border-border bg-muted px-1 font-medium font-mono text-muted-foreground text-xs shadow-xs">
<ArrowDownIcon className="size-2.5 opacity-80" />
</kbd>
<span>Navigate</span>
</div>
<div className="flex items-center gap-1.5">
<kbd className="pointer-events-none inline-flex h-5 min-w-5 items-center justify-center rounded border border-border bg-muted px-1 font-medium font-mono text-muted-foreground text-xs shadow-xs">
<CornerDownLeftIcon className="size-2.5 opacity-80" />
</kbd>
<span>Open</span>
</div>
</div>
<div className="flex items-center gap-1.5">
<kbd className="pointer-events-none inline-flex h-5 min-w-5 items-center justify-center rounded border border-border bg-muted px-1.5 font-medium font-mono text-muted-foreground text-xs shadow-xs">
Esc
</kbd>
<span>Close</span>
</div>
</CommandFooter>
</Command>
</CommandDialogPopup>
</CommandDialog>
);
};
export default CommandPaletteDemo;
Installation
npx shadcn@latest add @ark-cn/commandInstall the dependency required by this primitive:
npm install @ark-ui/react lucide-reactCopy the component source into your app:
TSXcomponents/ui/command.tsx
"use client";
import type { CollectionItem } from "@ark-ui/react/combobox";
import { Dialog } from "@ark-ui/react/dialog";
import { Portal } from "@ark-ui/react/portal";
import { SearchIcon } from "lucide-react";
import type { ComponentProps } from "react";
import {
Combobox,
ComboboxEmpty,
ComboboxGroupedList,
ComboboxInput,
type ComboboxInputProps,
ComboboxItem,
ComboboxItemGroup,
ComboboxItemGroupLabel,
ComboboxItemText,
ComboboxList,
type ComboboxListProps,
ComboboxPopup,
type ComboboxProps,
ComboboxSeparator,
} from "@/components/ui/combobox";
import { cn } from "@/lib/utils";
/**
* In-flow list: default Floating UI positioning takes the popup out of document
* flow, so a sibling footer in a flex column can sit on top and steal pointer
* events. This keeps the list in normal flow (dialog + standalone panel).
*/
const commandInlinePositioning: ComboboxProps["positioning"] = {
gutter: 0,
listeners: false,
placement: "bottom-start",
sameWidth: true,
updatePosition: async ({ floatingElement }) => {
if (!floatingElement) return;
const el = floatingElement;
el.style.setProperty("position", "relative");
el.style.setProperty("transform", "none");
el.style.setProperty("top", "auto");
el.style.setProperty("left", "auto");
el.style.setProperty("right", "auto");
el.style.setProperty("bottom", "auto");
el.style.setProperty("width", "100%");
el.style.setProperty("min-width", "0");
},
};
export const CommandDialog = (props: Dialog.RootProps) => (
<Dialog.Root {...props} />
);
export const CommandDialogTrigger = ({
className,
...props
}: Dialog.TriggerProps) => (
<Dialog.Trigger className={cn(className)} {...props} />
);
export const CommandDialogPortal = ({
...props
}: ComponentProps<typeof Portal>) => <Portal {...props} />;
export const CommandDialogBackdrop = ({
className,
...props
}: Dialog.BackdropProps) => (
<Dialog.Backdrop
className={cn(
"fixed inset-0 z-50 bg-black/40 backdrop-blur-sm transition-opacity duration-200 dark:bg-black/50 dark:backdrop-blur-md data-[state=closed]:opacity-0 data-[state=open]:opacity-100 supports-backdrop-filter:bg-black/25 supports-backdrop-filter:dark:bg-black/35",
className,
)}
{...props}
/>
);
export type CommandDialogPopupProps = Dialog.ContentProps & {
backdropClassName?: string;
positionerClassName?: string;
};
export const CommandDialogPopup = ({
backdropClassName,
children,
className,
positionerClassName,
...contentProps
}: CommandDialogPopupProps) => (
<Portal>
<CommandDialogBackdrop className={backdropClassName} />
<Dialog.Positioner
className={cn(
"fixed inset-0 z-50 flex items-start justify-center overscroll-y-none p-4 pt-[min(15vh,8rem)] sm:pt-24",
positionerClassName,
)}
>
<Dialog.Content
className={cn(
"relative z-50 flex max-h-[min(calc(100dvh-2rem),32rem)] min-h-0 min-w-0 w-full max-w-xl flex-col overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-lg outline-none transition-[opacity,transform] duration-150 data-[state=closed]:scale-[0.98] data-[state=closed]:opacity-0 data-[state=open]:scale-100 data-[state=open]:opacity-100",
className,
)}
{...contentProps}
>
{children}
</Dialog.Content>
</Dialog.Positioner>
</Portal>
);
export const Command = <T extends CollectionItem = CollectionItem>({
autoFocus = true,
className,
closeOnSelect = false,
disableLayer = true,
inputBehavior = "autohighlight",
positioning,
selectionBehavior = "clear",
...props
}: ComboboxProps<T>) => (
<Combobox
autoFocus={autoFocus}
className={cn(
"flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden",
className,
)}
closeOnSelect={closeOnSelect}
disableLayer={disableLayer}
inputBehavior={inputBehavior}
positioning={positioning ?? commandInlinePositioning}
selectionBehavior={selectionBehavior}
{...props}
/>
);
export const CommandInput = ({
className,
showClear = false,
showIndicator = false,
size = "lg",
startAddon = (
<SearchIcon
aria-hidden
className="size-4 text-muted-foreground opacity-80"
/>
),
...props
}: ComboboxInputProps) => (
<ComboboxInput
className={cn(
"shrink-0 rounded-none border-0 border-border border-b shadow-none has-focus-within:border-border has-focus-within:ring-0 has-focus-within:ring-offset-0",
className,
)}
showClear={showClear}
showIndicator={showIndicator}
size={size}
startAddon={startAddon}
{...props}
/>
);
export const CommandPopup = ({
className,
...props
}: ComponentProps<typeof ComboboxPopup>) => (
<ComboboxPopup
disablePortal
className={cn(
"max-h-none min-h-0 w-full min-w-0 flex-1 overflow-x-hidden overflow-y-auto rounded-none border-0 bg-transparent p-0 pb-1 shadow-none",
className,
)}
{...props}
/>
);
export const CommandList = <T extends CollectionItem = CollectionItem>({
className,
...props
}: ComboboxListProps<T>) => (
<ComboboxList
className={cn(
"max-h-[min(280px,var(--available-height,50vh))] gap-0 overflow-y-auto overscroll-contain p-1 outline-none",
className,
)}
{...props}
/>
);
export const CommandGroupedList = ComboboxGroupedList;
export const CommandEmpty = ({
className,
...props
}: ComponentProps<typeof ComboboxEmpty>) => (
<ComboboxEmpty
className={cn("rounded-none py-6 text-sm", className)}
{...props}
/>
);
export const CommandGroup = ComboboxItemGroup;
export const CommandGroupLabel = ({
className,
...props
}: ComponentProps<typeof ComboboxItemGroupLabel>) => (
<ComboboxItemGroupLabel
className={cn(
"px-2 py-1.5 text-[0.6875rem] uppercase tracking-wide",
className,
)}
{...props}
/>
);
export const CommandItem = ({
className,
...props
}: ComponentProps<typeof ComboboxItem>) => (
<ComboboxItem
className={cn(
"min-w-0 rounded-md px-2 py-2 hover:bg-accent hover:text-accent-foreground",
className,
)}
{...props}
/>
);
export const CommandItemText = ComboboxItemText;
export const CommandSeparator = ({
className,
...props
}: ComponentProps<"div">) => (
<ComboboxSeparator className={cn("my-2", className)} {...props} />
);
export const CommandShortcut = ({
className,
...props
}: ComponentProps<"span">) => (
<span
className={cn(
"ml-auto max-w-[45%] shrink-0 truncate text-end text-muted-foreground text-xs tracking-wide tabular-nums",
className,
)}
data-slot="command-shortcut"
{...props}
/>
);
export const CommandFooter = ({
className,
...props
}: ComponentProps<"div">) => (
<div
className={cn(
"shrink-0 flex flex-wrap items-center justify-between gap-3 border-border border-t bg-muted/30 px-3 py-2 text-muted-foreground text-xs",
className,
)}
data-slot="command-footer"
{...props}
/>
);
export const CommandPanel = ({
className,
...props
}: ComponentProps<"div">) => (
<div
className={cn(
"flex max-h-[min(24rem,70vh)] flex-col overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-md",
className,
)}
data-slot="command-panel"
{...props}
/>
);
export {
useDialog as useCommandDialog,
useDialogContext as useCommandDialogContext,
} from "@ark-ui/react/dialog";
Update import aliases to match your project setup.
Usage
import * as Command from "@/components/ui/command"Read exported parts in src/components/ui/command.tsx and compose the primitive according to the Ark UI pattern for this component.
Examples
Palette
Suggestions
Linear⌘L
Figma⌘F
Slack⌘S
Commands
Clipboard history⌘⇧C
Create snippet⌘N
System preferences⌘,
Navigate
Open
EscClose
import { ArrowDownIcon, ArrowUpIcon, CornerDownLeftIcon } from "lucide-react";
import { Fragment, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Command,
CommandDialog,
CommandDialogPopup,
CommandDialogTrigger,
CommandEmpty,
CommandFooter,
CommandGroup,
CommandGroupedList,
CommandGroupLabel,
CommandInput,
CommandItem,
CommandItemText,
CommandPopup,
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command";
type CommandPaletteItem = {
label: string;
value: string;
shortcut?: string;
group: "Suggestions" | "Commands";
};
const COMMAND_PALETTE_ITEMS: CommandPaletteItem[] = [
{ group: "Suggestions", label: "Linear", shortcut: "⌘L", value: "linear" },
{ group: "Suggestions", label: "Figma", shortcut: "⌘F", value: "figma" },
{ group: "Suggestions", label: "Slack", shortcut: "⌘S", value: "slack" },
{
group: "Commands",
label: "Clipboard history",
shortcut: "⌘⇧C",
value: "clipboard",
},
{
group: "Commands",
label: "Create snippet",
shortcut: "⌘N",
value: "snippet",
},
{
group: "Commands",
label: "System preferences",
shortcut: "⌘,",
value: "prefs",
},
];
const CommandPaletteDemo = () => {
const [open, setOpen] = useState(false);
const [resetKey, setResetKey] = useState(0);
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "b" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((o) => !o);
}
};
document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down);
}, []);
return (
<CommandDialog
onOpenChange={(d) => {
setOpen(d.open);
if (d.open) setResetKey((k) => k + 1);
}}
open={open}
>
<CommandDialogTrigger asChild>
<Button variant="outline">
Open command palette
<span className="ms-1 hidden text-muted-foreground text-xs sm:inline">
⌘b
</span>
</Button>
</CommandDialogTrigger>
<CommandDialogPopup aria-label="Command palette">
<Command
key={resetKey}
groupBy={(item) => item.group}
groupSort={["Suggestions", "Commands"]}
items={COMMAND_PALETTE_ITEMS}
open={open}
onOpenChange={(d) => {
if (!d.open) setOpen(false);
}}
onValueChange={() => {
setOpen(false);
}}
>
<CommandInput placeholder="Search for apps and commands…" />
<CommandPopup>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroupedList items={COMMAND_PALETTE_ITEMS}>
{([group, groupItems]) => (
<Fragment key={group}>
<CommandGroup>
<CommandGroupLabel>{group}</CommandGroupLabel>
{groupItems.map((item) => (
<CommandItem key={item.value} item={item}>
<CommandItemText>{item.label}</CommandItemText>
{item.shortcut ? (
<CommandShortcut>{item.shortcut}</CommandShortcut>
) : null}
</CommandItem>
))}
</CommandGroup>
{group !== "Commands" ? <CommandSeparator /> : null}
</Fragment>
)}
</CommandGroupedList>
</CommandPopup>
<CommandFooter>
<div className="flex flex-wrap items-center gap-4">
<div className="flex items-center gap-1.5">
<kbd className="pointer-events-none inline-flex h-5 items-center justify-center rounded border border-border bg-muted px-1 font-medium font-mono text-muted-foreground text-xs shadow-xs">
<ArrowUpIcon className="size-2.5 opacity-80" />
</kbd>
<kbd className="pointer-events-none inline-flex h-5 items-center justify-center rounded border border-border bg-muted px-1 font-medium font-mono text-muted-foreground text-xs shadow-xs">
<ArrowDownIcon className="size-2.5 opacity-80" />
</kbd>
<span>Navigate</span>
</div>
<div className="flex items-center gap-1.5">
<kbd className="pointer-events-none inline-flex h-5 min-w-5 items-center justify-center rounded border border-border bg-muted px-1 font-medium font-mono text-muted-foreground text-xs shadow-xs">
<CornerDownLeftIcon className="size-2.5 opacity-80" />
</kbd>
<span>Open</span>
</div>
</div>
<div className="flex items-center gap-1.5">
<kbd className="pointer-events-none inline-flex h-5 min-w-5 items-center justify-center rounded border border-border bg-muted px-1.5 font-medium font-mono text-muted-foreground text-xs shadow-xs">
Esc
</kbd>
<span>Close</span>
</div>
</CommandFooter>
</Command>
</CommandDialogPopup>
</CommandDialog>
);
};
export default CommandPaletteDemo;
Standalone
Profile
Settings
Billing
Log out
import {
Command,
CommandEmpty,
CommandInput,
CommandItem,
CommandItemText,
CommandList,
CommandPanel,
CommandPopup,
} from "@/components/ui/command";
const COMMAND_STANDALONE_ITEMS = [
{ label: "Profile", value: "profile" },
{ label: "Settings", value: "settings" },
{ label: "Billing", value: "billing" },
{ label: "Log out", value: "logout" },
] as const;
const CommandStandaloneDemo = () => (
<CommandPanel className="max-w-md">
<Command autoFocus={false} items={[...COMMAND_STANDALONE_ITEMS]} open>
<CommandInput placeholder="Type a command…" />
<CommandPopup>
<CommandEmpty>No results found.</CommandEmpty>
<CommandList items={[...COMMAND_STANDALONE_ITEMS]}>
{(item) => (
<CommandItem key={item.value} item={item}>
<CommandItemText>{item.label}</CommandItemText>
</CommandItem>
)}
</CommandList>
</CommandPopup>
</Command>
</CommandPanel>
);
export default CommandStandaloneDemo;
API reference
This component mirrors the upstream Ark UI primitive. All props and DOM behavior are defined by Ark unless you see an ark-cn-only row below.
CommandDialogPopup
| Prop | Type | Description |
|---|---|---|
| backdropClassName? | string | Extra class on the backdrop. |
| positionerClassName? | string | Extra class on the positioner wrapper. |
See the ARK UI documentation for the full API.