Command
A command palette that allows users to search and execute commands.
Preview
Loading...
import * as React from "react";
import {
ArrowUpIcon,
ArrowDownIcon,
ArrowTopRightIcon,
} from "@radix-ui/react-icons";
import { Button } from "@/components/ui/Button";
import {
Command,
CommandCollection,
CommandDialog,
CommandDialogPopup,
CommandDialogTrigger,
CommandEmpty,
CommandFooter,
CommandGroup,
CommandGroupLabel,
CommandInput,
CommandItem,
CommandList,
CommandPanel,
CommandSeparator,
CommandShortcut,
} from "@/components/ui/Command";
import { Kbd, KbdGroup } from "@/components/ui/Kbd";
export function CommandDemo() {
return {
const [open, setOpen] = React.useState(false);
function handleItemClick(_item: Item) {
setOpen(false);
}
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "j" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((open) => !open);
console.log("down");
}
};
document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down);
}, []);
return (
<CommandDialog onOpenChange={setOpen} open={open}>
<CommandDialogTrigger render={<Button variant="pill" />}>
Open Command Palette
<Kbd className="ml-0.5">⌘J</Kbd>
</CommandDialogTrigger>
<CommandDialogPopup>
<Command items={groupedItems}>
<CommandInput placeholder="Search for apps and commands..." />
<CommandPanel>
<CommandEmpty>No results found.</CommandEmpty>
<CommandList>
{(group: Group, _index: number) => (
<React.Fragment key={group.value}>
<CommandGroup items={group.items}>
<CommandGroupLabel>{group.value}</CommandGroupLabel>
<CommandCollection>
{(item: Item) => (
<CommandItem
key={item.value}
onClick={() => handleItemClick(item)}
value={item.value}
>
<span className="flex-1">{item.label}</span>
{item.shortcut && (
<CommandShortcut>{item.shortcut}</CommandShortcut>
)}
</CommandItem>
)}
</CommandCollection>
</CommandGroup>
<CommandSeparator />
</React.Fragment>
)}
</CommandList>
</CommandPanel>
<CommandFooter>
<div className="flex items-center gap-1">
<div className="flex items-center gap-0.5">
<KbdGroup>
<Kbd>
<ArrowUpIcon />
</Kbd>
<Kbd>
<ArrowDownIcon />
</Kbd>
</KbdGroup>
<span>Navigate</span>
</div>
<div className="flex items-center gap-0.5">
<Kbd>↵</Kbd>
<span>Open</span>
</div>
</div>
<div className="flex items-center gap-0.5">
<KbdGroup>
<Kbd>⌘</Kbd>
<Kbd>K</Kbd>
</KbdGroup>
<span>Close</span>
</div>
</CommandFooter>
</Command>
</CommandDialogPopup>
</CommandDialog>
);
};Installation
Make sure that namespace is set in your component.json file. Namespace docs: Learn more about namespaces
pnpm dlx shadcn@latest add @aura/commandManual
Install the following dependencies:
pnpm install @base-ui/react @radix-ui/react-iconsCopy and paste the autocomplete component into your components/ui/Autocomplete.tsx file.
"use client";
/**
* @description Displays an autocomplete input with a list of options.
*/
import * as React from "react";
import { Autocomplete as AutocompletePrimitive } from "@base-ui/react/autocomplete";
import { Cross1Icon, CaretSortIcon } from "@radix-ui/react-icons";
import { cn } from "@/utils/class-names";
import { Input } from "@/components/ui/Input";
import { ScrollArea } from "@/components/ui/ScrollArea";
function Autocomplete({
...props
}: React.ComponentProps<typeof AutocompletePrimitive.Root>) {
return <AutocompletePrimitive.Root data-slot="autocomplete" {...props} />;
}
function AutocompleteInput({
className,
showTrigger = false,
showClear = false,
startAddon,
size,
...props
}: Omit<AutocompletePrimitive.Input.Props, "size"> & {
showTrigger?: boolean;
showClear?: boolean;
startAddon?: React.ReactNode;
size?: "sm" | "default" | "lg" | number;
ref?: React.Ref<HTMLInputElement>;
}) {
const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number;
return (
<div className="relative w-full">
{startAddon && (
<div
aria-hidden="true"
className="[&_svg]:-mx-0.5 pointer-events-none absolute inset-y-0 start-px z-10 flex items-center ps-[calc(--spacing(3)-1px)] opacity-80 has-[+[data-size=sm]]:ps-[calc(--spacing(2.5)-1px)] [&_svg:not([class*='size-'])]:size-1.5 sm:[&_svg:not([class*='size-'])]:size-1"
data-slot="autocomplete-start-addon"
>
{startAddon}
</div>
)}
<AutocompletePrimitive.Input
className={cn(
startAddon &&
"data-[size=sm]:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(7.5)-1px)] *:data-[slot=autocomplete-input]:ps-[calc(--spacing(8.5)-1px)] sm:data-[size=sm]:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(7)-1px)] sm:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(8)-1px)]",
sizeValue === "sm"
? "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-6.5"
: "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-7",
className,
)}
data-slot="autocomplete-input"
data-size={typeof sizeValue === "string" ? sizeValue : undefined}
render={<Input />}
{...props}
/>
{showTrigger && (
<AutocompleteTrigger
className={cn(
"-translate-y-1/2 absolute top-1/2 inline-flex size-2.5 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-colors pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=autocomplete-clear]]:hidden sm:size-2 [&_svg:not([class*='size-'])]:size-1.5 sm:[&_svg:not([class*='size-'])]:size-1 [&_svg]:pointer-events-none [&_svg]:shrink-0",
sizeValue === "sm" ? "end-0" : "end-0.5",
)}
>
<CaretSortIcon />
</AutocompleteTrigger>
)}
{showClear && (
<AutocompleteClear
className={cn(
"-translate-y-1/2 absolute top-1/2 inline-flex size-2.5 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-colors pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=autocomplete-clear]]:hidden sm:size-2 [&_svg:not([class*='size-'])]:size-1.5 sm:[&_svg:not([class*='size-'])]:size-1 [&_svg]:pointer-events-none [&_svg]:shrink-0",
sizeValue === "sm" ? "end-0" : "end-0.5",
)}
>
<Cross1Icon />
</AutocompleteClear>
)}
</div>
);
}
function AutocompletePopup({
className,
children,
sideOffset = 4,
...props
}: AutocompletePrimitive.Popup.Props & {
sideOffset?: number;
}) {
return (
<AutocompletePrimitive.Portal>
<AutocompletePrimitive.Positioner
className="z-50 select-none bg-gray-1 border border-gray-a6 rounded-sm relative shadow-md"
data-slot="autocomplete-positioner"
sideOffset={sideOffset}
>
<span
className={cn(
className,
"bg-gray-1 border border-gray-a6 rounded-sm relative shadow-md"
)}
>
<AutocompletePrimitive.Popup
className="flex max-h-[min(var(--available-height),23rem)] w-(--anchor-width) max-w-(--available-width) flex-col"
data-slot="autocomplete-popup"
{...props}
>
{children}
</AutocompletePrimitive.Popup>
</span>
</AutocompletePrimitive.Positioner>
</AutocompletePrimitive.Portal>
);
}
function AutocompleteItem({
className,
children,
...props
}: AutocompletePrimitive.Item.Props) {
return (
<AutocompletePrimitive.Item
data-slot="autocomplete-item"
className={cn(
className,
"p-0.5 px-2 hover:bg-accent-3 data-[highlighted]:bg-accent-3 flex relative cursor-pointer justify-between data-disabled:pointer-events-none data-disabled:opacity-64"
)}
{...props}
>
{children}
</AutocompletePrimitive.Item>
);
}
function AutocompleteSeparator({
className,
...props
}: AutocompletePrimitive.Separator.Props) {
return (
<AutocompletePrimitive.Separator
data-slot="autocomplete-separator"
className={cn(className, "m-0.6 h-px bg-gray-a6")}
{...props}
/>
);
}
function AutocompleteGroup({
...props
}: AutocompletePrimitive.Group.Props) {
return <AutocompletePrimitive.Group data-slot="autocomplete-group" {...props} />;
}
function AutocompleteGroupLabel({
className,
...props
}: AutocompletePrimitive.GroupLabel.Props) {
return (
<AutocompletePrimitive.GroupLabel
data-slot="autocomplete-group-label"
className={cn(className, "p-0.5 px-2 text-gray-12 font-medium text-xs")}
{...props}
/>
);
}
function AutocompleteEmpty({
className,
...props
}: AutocompletePrimitive.Empty.Props) {
return (
<AutocompletePrimitive.Empty
data-slot="autocomplete-empty"
className={cn(className, "p-0.5 px-2 text-center text-gray-12 empty:hidden")}
{...props}
/>
);
}
function AutocompleteRow({
className,
...props
}: AutocompletePrimitive.Row.Props) {
return (
<AutocompletePrimitive.Row
className={className}
data-slot="autocomplete-row"
{...props}
/>
);
}
function AutocompleteValue({ ...props }: AutocompletePrimitive.Value.Props) {
return (
<AutocompletePrimitive.Value data-slot="autocomplete-value" {...props} />
);
}
function AutocompleteList({
className,
...props
}: AutocompletePrimitive.List.Props) {
return (
<ScrollArea>
<AutocompletePrimitive.List
data-slot="autocomplete-list"
className={cn(className)}
{...props}
/>
</ScrollArea>
);
}
function AutocompleteClear({
className,
...props
}: AutocompletePrimitive.Clear.Props) {
return (
<AutocompletePrimitive.Clear
className={cn(
"-translate-y-1/2 absolute end-0.5 top-1/2 inline-flex size-2.5 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-[color,background-color,box-shadow,opacity] pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 sm:size-2 [&_svg:not([class*='size-'])]:size-1.5 sm:[&_svg:not([class*='size-'])]:size-1 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
data-slot="autocomplete-clear"
{...props}
>
<Cross1Icon />
</AutocompletePrimitive.Clear>
);
}
function AutocompleteStatus({
className,
...props
}: AutocompletePrimitive.Status.Props) {
return (
<AutocompletePrimitive.Status
data-slot="autocomplete-status"
className={cn(className, "p-0.5 px-2 font-medium text-gray-12 text-xs empty:m-0 empty:p-0")}
{...props}
/>
);
}
function AutocompleteCollection({
...props
}: AutocompletePrimitive.Collection.Props) {
return (
<AutocompletePrimitive.Collection
data-slot="autocomplete-collection"
{...props}
/>
);
}
function AutocompleteTrigger({
className,
...props
}: AutocompletePrimitive.Trigger.Props) {
return (
<AutocompletePrimitive.Trigger
data-slot="autocomplete-trigger"
className={cn(className)}
{...props}
/>
);
}
export {
Autocomplete,
AutocompleteInput,
AutocompleteTrigger,
AutocompletePopup,
AutocompleteItem,
AutocompleteSeparator,
AutocompleteGroup,
AutocompleteGroupLabel,
AutocompleteEmpty,
AutocompleteValue,
AutocompleteList,
AutocompleteClear,
AutocompleteStatus,
AutocompleteRow,
AutocompleteCollection,
};Copy and paste the class names utility into your utils/class-names.ts file.
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Copy and paste the Command component into your components/ui/Command.tsx file.
"use client";
import { Dialog as CommandDialogPrimitive } from "@base-ui/react/dialog";
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
import * as React from "react";
import { cn } from "@/utils/class-names";
import {
Autocomplete,
AutocompleteCollection,
AutocompleteEmpty,
AutocompleteGroup,
AutocompleteGroupLabel,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompleteSeparator,
} from "@/components/ui/Autocomplete";
const CommandInputContext = React.createContext<{
inputRef: React.RefObject<HTMLInputElement | null> | null;
}>({
inputRef: null,
});
const CommandDialog = CommandDialogPrimitive.Root;
const CommandDialogPortal = CommandDialogPrimitive.Portal;
function CommandDialogTrigger(props: CommandDialogPrimitive.Trigger.Props) {
return (
<CommandDialogPrimitive.Trigger
data-slot="command-dialog-trigger"
{...props}
/>
);
}
function CommandDialogBackdrop({
className,
...props
}: CommandDialogPrimitive.Backdrop.Props) {
return (
<CommandDialogPrimitive.Backdrop
className={cn(
"fixed inset-0 z-50 bg-gray-9a backdrop-blur-xs transition-all",
className,
)}
data-slot="command-dialog-backdrop"
{...props}
/>
);
}
function CommandDialogViewport({
className,
...props
}: CommandDialogPrimitive.Viewport.Props) {
return (
<CommandDialogPrimitive.Viewport
className={cn(
"fixed inset-0 z-50 flex flex-col items-center px-2 py-[max(--spacing(4),4vh)] sm:py-[10vh]",
className,
)}
data-slot="command-dialog-viewport"
{...props}
/>
);
}
function CommandDialogPopup({
className,
children,
...props
}: CommandDialogPrimitive.Popup.Props) {
const inputRef = React.useRef<HTMLInputElement>(null);
return (
<CommandDialogPortal>
<CommandDialogBackdrop />
<CommandDialogViewport>
<CommandDialogPrimitive.Popup
className={cn(
"-translate-y-[calc(1.25rem*var(--nested-dialogs))] relative row-start-2 flex max-h-100 min-h-0 w-full min-w-0 max-w-xl scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border border-gray-a6 bg-gray-1 bg-clip-padding text-gray-12 opacity-[calc(1-0.1*var(--nested-dialogs))] shadow-lg transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:bg-gray-a2 data-nested:data-ending-style:translate-y-8 data-nested:data-starting-style:translate-y-8 data-nested-dialog-open:origin-top data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 **:data-[slot=scroll-area-viewport]:data-has-overflow-y:pe-1",
className,
)}
data-slot="command-dialog-popup"
initialFocus={inputRef}
{...props}
>
<CommandInputContext.Provider value={{ inputRef }}>
{children}
</CommandInputContext.Provider>
</CommandDialogPrimitive.Popup>
</CommandDialogViewport>
</CommandDialogPortal>
);
}
function Command({
autoHighlight = "always",
keepHighlight = true,
open = true,
...props
}: React.ComponentProps<typeof Autocomplete>) {
return (
<Autocomplete
autoHighlight={autoHighlight}
keepHighlight={keepHighlight}
open={open}
{...props}
/>
);
}
function CommandInput({
className,
placeholder = undefined,
...props
}: React.ComponentProps<typeof AutocompleteInput>) {
const { inputRef } = React.useContext(CommandInputContext);
return (
<div className="px-1.5 py-0.5">
<AutocompleteInput
className={cn(
"border-transparent! bg-transparent shadow-none before:hidden has-focus-visible:ring-0 pl-5",
className,
)}
placeholder={placeholder}
ref={inputRef}
size="lg"
startAddon={<MagnifyingGlassIcon />}
{...props}
/>
</div>
);
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof AutocompleteList>) {
return (
<AutocompleteList
className={cn("not-empty:scroll-py-2 not-empty:p-2", className)}
data-slot="command-list"
{...props}
/>
);
}
function CommandEmpty({
className,
...props
}: React.ComponentProps<typeof AutocompleteEmpty>) {
return (
<AutocompleteEmpty
className={cn("empty:hidden not-empty:py-6", className)}
data-slot="command-empty"
{...props}
/>
);
}
function CommandPanel({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className="-mx-px relative min-h-0 rounded-t-xl border border-gray-a6 bg-gray-1 bg-clip-padding [clip-path:inset(0_1px)] before:pointer-events-none before:absolute before:inset-0 before:rounded-t-[calc(var(--radius-xl)-1px)] **:data-[slot=scroll-area-scrollbar]:mt-2"
{...props}
/>
);
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof AutocompleteGroup>) {
return (
<AutocompleteGroup
className={className}
data-slot="command-group"
{...props}
/>
);
}
function CommandGroupLabel({
className,
...props
}: React.ComponentProps<typeof AutocompleteGroupLabel>) {
return (
<AutocompleteGroupLabel
className={className}
data-slot="command-group-label"
{...props}
/>
);
}
function CommandCollection({
...props
}: React.ComponentProps<typeof AutocompleteCollection>) {
return <AutocompleteCollection data-slot="command-collection" {...props} />;
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof AutocompleteItem>) {
return (
<AutocompleteItem
className={cn("py-1.5", className)}
data-slot="command-item"
{...props}
/>
);
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof AutocompleteSeparator>) {
return (
<AutocompleteSeparator
className={cn("my-2", className)}
data-slot="command-separator"
{...props}
/>
);
}
function CommandShortcut({ className, ...props }: React.ComponentProps<"kbd">) {
return (
<span
className={cn(
"ms-auto font-medium text-gray-11 text-xs tracking-widest",
className,
)}
data-slot="command-shortcut"
{...props}
/>
);
}
function CommandFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"flex items-center justify-between gap-1 rounded-b-[calc(var(--radius-2xl)-1px)] border-t border-gray-a6 bg-gray-2 px-2 py-1 text-gray-11 text-xs z-10",
className,
)}
data-slot="command-footer"
{...props}
/>
);
}
export {
Command,
CommandCollection,
CommandDialog,
CommandDialogPopup,
CommandDialogTrigger,
CommandEmpty,
CommandFooter,
CommandGroup,
CommandGroupLabel,
CommandInput,
CommandItem,
CommandList,
CommandPanel,
CommandSeparator,
CommandShortcut,
};