Primitives
Command
Command palette with token-matched dialog, input, and result rows.
Composition
Inline command list
- <Command>
- <CommandInput />
- <CommandList>
- <CommandLoading />
- <CommandEmpty />
- <CommandGroup>
- <CommandItem>
- <CommandShortcut />
- <CommandItem>
- <CommandSeparator />
Command palette dialog
CommandDialog supplies the dialog and Command root.
- <CommandDialog>
- <CommandInput />
- <CommandList>
- <CommandEmpty />
- <CommandGroup>
- <CommandItem />
Installation
First install and activate one skin. Core deliberately contains no visual token defaults.
The Control UI source installs this primitive from src/registry/sources/control-ui/ui/command.tsx.
npx shadcn@latest add https://control-ui.dev/r/command.jsonDependencies
Surface variants
Dialogsrc/registry/sources/control-ui/surface-variants.tsSupportdialogPrimitiveEmptyemptyPrimitiveLive statuslive-statusPrimitiveScroll areascroll-areaPrimitiveRaw code
Primary installed source
"use client";import { Command as CommandPrimitive, useCommandState } from "cmdk";import { type ComponentProps, type CSSProperties, type ReactNode, useEffect, useRef } from "react";import type { PopupKnobStyle } from "@/components/control-ui/knob-contracts/popup-knobs";import { cn } from "@/components/control-ui/lib/cn";import { popupItemStructureClasses } from "@/components/control-ui/surface-variants";import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/control-ui/ui/dialog";import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/control-ui/ui/empty";import { LiveStatus } from "@/components/control-ui/ui/live-status";import { ScrollArea } from "@/components/control-ui/ui/scroll-area";export type CommandChrome = "standalone" | "embedded";export type CommandProps = Omit<ComponentProps<typeof CommandPrimitive>, "style"> & { chrome?: CommandChrome; style?: CSSProperties & PopupKnobStyle;};export function Command({ chrome = "standalone", className, ...props }: CommandProps) { return ( <CommandPrimitive data-control-ui="command" data-popup-kind="command" data-slot="root" data-control-family="popup" data-popup-part={chrome === "standalone" ? "surface" : undefined} data-popup-static="" data-chrome={chrome} data-surface={chrome === "standalone" ? "panel" : undefined} className={cn("flex h-full w-full flex-col", className)} {...props} /> );}export function CommandDialog({ title = "Command palette", description = "Search for a command to run…", children, className, commandProps, ...props}: ComponentProps<typeof Dialog> & { title?: string; description?: string; className?: string; commandProps?: ComponentProps<typeof Command>;}) { return ( <Dialog {...props}> <DialogContent className={cn("p-0", className)} showCloseButton={false}> <DialogTitle className="sr-only">{title}</DialogTitle> <DialogDescription className="sr-only">{description}</DialogDescription> <Command chrome="embedded" label={title} {...commandProps} data-control-ui="command" data-popup-kind="command" data-control-family="popup" data-slot="dialog-root" className={cn("grid grid-rows-[auto_1fr] in-data-[starting-style]:grid-rows-[auto_0fr]", commandProps?.className)} > {children} </Command> </DialogContent> </Dialog> );}export type CommandInputProps = Omit<ComponentProps<typeof CommandPrimitive.Input>, "style"> & { style?: CSSProperties & PopupKnobStyle };export function CommandInput({ className, ...props }: CommandInputProps) { const hasResults = useCommandState((state) => state.filtered.count > 0); return ( <div data-control-ui="command" data-popup-kind="command" data-slot="input-wrapper" data-control-family="popup" data-control="true" data-size="lg" className="flex items-center" > <svg data-control-ui="command" data-popup-kind="command" data-control-family="popup" data-slot="input-icon" viewBox="0 0 16 16" className="size-4 shrink-0" aria-hidden="true" fill="none" > <circle cx="7" cy="7" r="4.25" stroke="currentColor" strokeWidth="1.3" /> <path d="m10.5 10.5 3 3" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" /> </svg> <CommandPrimitive.Input asChild data-control-ui="command" data-popup-kind="command" data-control-family="popup" data-slot="input" className={cn("h-full w-full disabled:cursor-not-allowed", className)} {...props} > <input role="combobox" aria-expanded={hasResults} /> </CommandPrimitive.Input> </div> );}function CommandListSurface({ hasResults, ...props }: ComponentProps<"div"> & { hasResults: boolean }) { return <div {...props} role={hasResults ? "listbox" : "group"} />;}function defaultResultsLabel(count: number) { if (count === 0) return "No results found"; return count === 1 ? "1 result" : `${count} results`;}export type CommandListProps = Omit<ComponentProps<typeof CommandPrimitive.List>, "style"> & { style?: CSSProperties & PopupKnobStyle; getResultsLabel?: (count: number) => string;};export function CommandList({ className, children, getResultsLabel = defaultResultsLabel, ...props }: CommandListProps) { const count = useCommandState((state) => state.filtered.count); const search = useCommandState((state) => state.search); const hasResults = count > 0; const viewportRef = useRef<HTMLDivElement>(null); // biome-ignore lint/correctness/useExhaustiveDependencies: search is the trigger; cmdk scrolls the stale selection on search and a zero-height viewport on open, so reset the next frame. useEffect(() => { const frame = requestAnimationFrame(() => viewportRef.current?.scrollTo({ top: 0 })); return () => cancelAnimationFrame(frame); }, [search]); return ( <> <ScrollArea viewportRef={viewportRef} maxHeight="min(360px, var(--available-height, 360px))" className="min-h-0 w-full"> <CommandPrimitive.List asChild data-control-ui="command" data-control-family="popup" data-popup-kind="command" data-slot="list" className={cn("p-[var(--popover-padding)]", className)} {...props} > <CommandListSurface hasResults={hasResults}>{children}</CommandListSurface> </CommandPrimitive.List> </ScrollArea> <LiveStatus message={search ? getResultsLabel(count) : ""} /> </> );}export type CommandEmptyProps = Omit<ComponentProps<typeof CommandPrimitive.Empty>, "style" | "title"> & { style?: CSSProperties & PopupKnobStyle; title?: ReactNode; description?: ReactNode;};export function CommandEmpty({ className, children, title = "No results found", description = "Try a different search term.", ...props}: CommandEmptyProps) { return ( <CommandPrimitive.Empty asChild data-control-ui="command" data-control-family="popup" data-popup-kind="command" data-slot="empty" className={className} {...props} > <div> {children ?? ( <Empty> <EmptyHeader> <EmptyMedia> <CommandEmptyIcon /> </EmptyMedia> <EmptyTitle>{title}</EmptyTitle> {description ? <EmptyDescription>{description}</EmptyDescription> : null} </EmptyHeader> </Empty> )} </div> </CommandPrimitive.Empty> );}export type CommandLoadingProps = Omit<ComponentProps<typeof CommandPrimitive.Loading>, "style"> & { style?: CSSProperties & PopupKnobStyle;};export function CommandLoading({ label = "Loading results", className, ...props }: CommandLoadingProps) { return ( <CommandPrimitive.Loading data-control-ui="command" data-control-family="popup" data-popup-kind="command" data-slot="loading" label={label} className={className} {...props} /> );}function CommandEmptyIcon() { return ( <svg viewBox="0 0 16 16" aria-hidden="true" fill="none"> <circle cx="7" cy="7" r="4.25" stroke="currentColor" strokeWidth="1.3" /> <path d="m10.5 10.5 3 3" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" /> </svg> );}export type CommandGroupProps = Omit<ComponentProps<typeof CommandPrimitive.Group>, "style"> & { style?: CSSProperties & PopupKnobStyle };export function CommandGroup({ className, ...props }: CommandGroupProps) { return ( <CommandPrimitive.Group data-control-ui="command" data-control-family="popup" data-popup-kind="command" data-slot="group" className={className} {...props} /> );}export type CommandSeparatorProps = Omit<ComponentProps<typeof CommandPrimitive.Separator>, "style"> & { style?: CSSProperties & PopupKnobStyle;};export function CommandSeparator({ className, ...props }: CommandSeparatorProps) { return ( <CommandPrimitive.Separator aria-hidden="true" data-control-ui="command" data-popup-kind="command" data-control-family="popup" data-popup-part="separator" data-slot="separator" className={cn(className)} {...props} /> );}export type CommandItemProps = Omit<ComponentProps<typeof CommandPrimitive.Item>, "style"> & { style?: CSSProperties & PopupKnobStyle };export function CommandItem({ className, disabled, onSelect, ...props }: CommandItemProps) { function handleSelect(value: string) { if (disabled) return; onSelect?.(value); } return ( <CommandPrimitive.Item data-control-ui="command" data-popup-kind="command" data-control-family="popup" data-popup-part="item" data-slot="item" className={cn("relative", popupItemStructureClasses, className)} {...props} data-disabled={disabled ? "true" : undefined} aria-disabled={disabled || undefined} disabled={disabled} onSelect={handleSelect} /> );}export type CommandShortcutProps = Omit<ComponentProps<"span">, "style"> & { style?: CSSProperties & PopupKnobStyle };export function CommandShortcut({ className, ...props }: CommandShortcutProps) { return ( <span data-control-ui="command" data-popup-kind="command" data-control-family="popup" data-popup-part="shortcut" data-slot="shortcut" aria-hidden="true" className={cn(className)} {...props} /> );}Knobs
Typed custom properties the recipe paints with. Set one on the root — style, a utility class, or a skin — and every slot inherits it.
--cui-popup-* · 44 knobsHow the cascade resolves--cui-popup-title-foreground<color>var(--cui-popup-foreground)--cui-popup-title-font-size<length-percentage>var(--text-heading-3)--cui-popup-title-font-weight<number>var(--font-weight-semibold)--cui-popup-title-line-height<number>var(--text-heading-3--line-height)--cui-popup-title-letter-spacing<length>-0.025em--cui-popup-description-foreground<color>var(--muted-foreground)--cui-popup-close-inset<length-percentage>calc(var(--spacing) * 3)--cui-popup-item-inset<length-percentage>calc(var(--spacing) * 8)--cui-popup-label-padding-block<length-percentage>var(--cui-popup-item-padding-block)--cui-popup-label-padding-inline<length-percentage>var(--cui-popup-item-padding-inline)--cui-popup-label-font-size<length-percentage>var(--text-micro)--cui-popup-label-font-weight<number>var(--font-weight-medium)--cui-popup-label-letter-spacing<length>0.08em--cui-popup-label-text-transformnone | uppercase | lowercase | capitalizeuppercase--cui-popup-separator-inset<length-percentage>calc(var(--popover-padding) * -1)--cui-popup-shortcut-font-size<length-percentage>var(--text-body)--cui-popup-shortcut-letter-spacing<length>0.02em--cui-popup-status-success-foreground<color>var(--success-text)--cui-popup-status-warning-foreground<color>var(--warning-text)--cui-popup-status-info-foreground<color>var(--info-text)--cui-popup-status-error-foreground<color>var(--destructive)--cui-popup-radius<length-percentage>var(--radius-popover)--cui-popup-background<color>var(--popover)--cui-popup-foreground<color>var(--popover-foreground)--cui-popup-border-color<color>var(--control-rim)--cui-popup-border-width<length>var(--control-rim-width)--cui-popup-shadow*var(--shadow-pop)--cui-popup-backdrop-filter*blur(var(--backdrop-blur-popover))--cui-popup-backdrop-background<color>oklch(from var(--foreground) l c h / var(--overlay-opacity))--cui-popup-backdrop-blur<length>var(--backdrop-blur-overlay)--cui-popup-item-radius<length-percentage>var(--radius-popup-item-fit)--cui-popup-item-font-size<length-percentage>var(--text-body)--cui-popup-item-icon-size<length-percentage>1rem--cui-popup-item-padding-block<length-percentage>calc(var(--spacing) * 1)--cui-popup-item-padding-inline<length-percentage>calc(var(--padding-x) * 0.5)--cui-popup-item-min-height<length-percentage>max(var(--target-min), var(--control-h-xs))--cui-popup-item-highlight-shadow*inset 0 0 0 1px oklch(from var(--foreground) l c h / 0.35)--cui-popup-item-foreground<color>var(--foreground)--cui-popup-item-highlight-background<color>var(--hover-fill)--cui-popup-item-highlight-foreground<color>var(--cui-popup-item-foreground)--cui-popup-item-highlight-muted-foreground<color>var(--muted-foreground)--cui-popup-item-disabled-opacity<number>var(--disabled-opacity)--cui-popup-separator-color<color>var(--border)--cui-popup-shortcut-foreground<color>var(--muted-foreground)