- Accordion
- Agent Thinking
- Alert
- Alert Dialog
- Announcement
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Checkbox
- Checkbox Card
- Code Block
- Collapsible
- Color Picker
- Combobox
- Command
- Composer
- Composer Loader
- Context Menu
- Data Grid
- Data Table
- Date Picker
- Dialog
- Direction
- Drawer
- Dropdown Menu
- Empty
- Field
- File Upload
- Hover Card
- Input
- Input Group
- Input Otp
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Meter
- Native Select
- Navigation Menu
- Number Field
- Pagination
- Period Switcher
- Popover
- Progress
- Questionnaire
- Radio Card
- Radio Group
- Resizable
- Reveal
- Scroll Area
- Search Field
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Social Button
- Sonner
- Spinner
- Status Dot
- Switch
- Switch Card
- Table
- Tabs
- Tag Group
- Textarea
- Theme Toggle
- Time Field
- Toggle
- Toggle Group
- Tooltip
- Trace
- Tree View
- Agent Creator 01
- Agent Limits Card 01
- Agent Progress 01
- AI Chat 01
- AI Image Generation 01
- AI Profile 01
- Alerts Card 01
- Auth Card 01
- Calendar 01
- Composer 01
- Composer Attachments 01
- Composer Panel 01
- Finance Dashboard 01
- Home Dashboard 01
- Marketing Dashboard 01
- Medical Profile 01
- Meeting Scheduler 01
- Multi Agent Chat 01
- Notification Center 01
- Patient Info Card 01
- Project Board 01
- Questionnaire 01
- Settings Modal 01
- Sidebar 01
- Stat Cards 01
- Task List 01
- Web Search 01
- Activity Rings Card 01
- Area Chart Card 01
- Bar List Card 01
- Combo Chart Card 01
- Contributions Card 01
- Earnings Chart Card 01
- Funnel Chart Card 01
- Heatmap Chart Card 01
- Line Chart Card 01
- Most Active Days Card 01
- Orders Chart Card 01
- Radar Chart Card 01
- Radial Chart Card 01
- Revenue Chart Card 01
- Sankey Chart Card 01
- Scatter Chart Card 01
- Sleep Score Card 01
- Stage Bars Card 01
- Steps Card 01
Installation
$ pnpm dlx shadcn@latest add @cobalt/command
Usage
Command is the panel surface: a 24px-radius card on the primary surface, a search row over a scrolling list, and 12px rows that step onto the secondary surface while selected. CommandDialog puts the same list in a top-anchored palette over a black/20 scrim, with the lift-and-sharpen motion of the measured source.
<CommandDialog open={open} onOpenChange={setOpen}>
<Command>
<CommandInput placeholder="Search pages and actions..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Go to">
<CommandItem>
<HomeIcon />
<span>Dashboard</span>
<CommandShortcut>⌘1</CommandShortcut>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</CommandDialog>The rows filter with cmdk's own match, arrows move the highlight, Enter runs the selected row and Escape closes the dialog.
Trigger and footer
CommandTrigger is a pill that looks like a search field and opens the palette: the search icon and label on the left, the shortcut as a Kbd chip on the right. It is a Button underneath, so it takes onClick, disabled and the rest of the button props.
CommandFooter is a bar under the list inside CommandDialog, for key hints.
<CommandTrigger shortcut="⌘K" onClick={() => setOpen(true)}>
Quick search
</CommandTrigger>
<CommandDialog open={open} onOpenChange={setOpen}>
<Command>{/* ... */}</Command>
<CommandFooter>
<Kbd>↵</Kbd> Select
</CommandFooter>
</CommandDialog>Source
"use client";
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
import { RiSearchLine, RiCloseLine } from "@remixicon/react";
import { Command as CommandPrimitive } from "cmdk";
import { cn } from "cn";
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogDescription,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
} from "@/components/ui/dialog";
import { Kbd } from "@/components/ui/kbd";
// A searchable list of actions and destinations on a cmdk root, with the
// palette surface, typography and motion measured from an MIT-licensed
// command palette: a black/20 scrim, a 24px-radius panel 560px wide and
// 12vh from the top, a search row over a scrolling list with a progressive
// top blur, 12px-radius rows that step onto the secondary surface while
// selected, and the shortcut rendered as a kbd pill.
//
// The palette opens over 200ms: it fades in, lifts 10px and sharpens from a
// 4px blur; closing plays the same 200ms in reverse from 8px and a 3px blur.
// Rows change colour in 100ms. Reduced motion keeps the fade and drops the
// lift, scale and blur.
//
// Every colour references a token with a semantic fallback, so the component
// also works without theme-cobalt.
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"flex size-full max-h-[min(560px,76vh)] min-h-0 flex-col overflow-hidden rounded-[24px] border border-[color:var(--command-border,var(--border))] bg-[var(--surface-card,var(--popover))] p-0 text-[color:var(--foreground-strong,var(--popover-foreground))] shadow-[shadow:var(--command-shadow,0_1px_0_0_rgb(0_0_0/0.0196),0_1px_12px_0_rgb(0_0_0/0.0588),0_0_1px_0_rgb(0_0_0/0.3216))]",
// Inside CommandDialog the popup owns the surface, so the root only
// carries the layout.
"in-data-[slot=command-dialog-content]:rounded-none! in-data-[slot=command-dialog-content]:border-0! in-data-[slot=command-dialog-content]:bg-transparent! in-data-[slot=command-dialog-content]:shadow-none!",
className
)}
{...props}
/>
);
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
className,
showCloseButton = false,
...props
}: Omit<React.ComponentProps<typeof Dialog>, "children"> & {
title?: string;
description?: string;
className?: string;
showCloseButton?: boolean;
children: React.ReactNode;
}) {
return (
<Dialog {...props}>
<DialogPortal>
<DialogOverlay className="bg-black/20 duration-150 ease-out" />
<DialogPrimitive.Popup
data-slot="command-dialog-content"
className={cn(
// Positioned by the box itself so the motion can own the
// translate: inset-x-0 with mx-auto centres the 560px panel.
"fixed inset-x-0 top-[12vh] z-50 mx-auto flex max-h-[min(560px,76vh)] min-h-0 w-[calc(100%-2rem)] max-w-[560px] flex-col overflow-clip rounded-[24px] border border-[color:var(--command-border,var(--border))] bg-[var(--surface-card,var(--popover))] text-[color:var(--foreground-strong,var(--popover-foreground))] shadow-[shadow:var(--command-shadow,0_1px_0_0_rgb(0_0_0/0.0196),0_1px_12px_0_rgb(0_0_0/0.0588),0_0_1px_0_rgb(0_0_0/0.3216))] outline-none",
"transition-[opacity,translate,scale,filter] duration-200 ease-out",
"data-ending-style:opacity-0 data-starting-style:opacity-0",
"motion-safe:data-ending-style:translate-y-2 motion-safe:data-ending-style:scale-[0.98] motion-safe:data-ending-style:blur-[3px]",
"motion-safe:data-starting-style:translate-y-2.5 motion-safe:data-starting-style:scale-[0.98] motion-safe:data-starting-style:blur-[4px]",
"motion-reduce:transition-[opacity]",
className
)}
>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="command-dialog-close"
render={
<Button
variant="close"
size="icon-xs"
className="absolute top-3 right-3"
/>
}
>
<RiCloseLine className="size-6" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Popup>
</DialogPortal>
</Dialog>
);
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex shrink-0 items-center gap-2 border-b border-[color:var(--border-subtle,var(--border))] p-3"
>
<RiSearchLine className="size-5 shrink-0 text-[color:var(--icon-foreground,var(--muted-foreground))]" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"min-w-0 flex-1 bg-transparent text-sm font-medium text-[color:var(--foreground-strong,var(--popover-foreground))] outline-hidden placeholder:text-[color:var(--command-muted-foreground,var(--muted-foreground))] [&::-webkit-search-cancel-button]:hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
);
}
// The list fades in a progressive blur along its top edge once scrolled: a
// masked 1px backdrop blur over 24px, a 4px blur over the first 16px, and a
// wash of the panel surface. Each layer ramps over the first 24px of scroll,
// so the resting state keeps a plain top edge.
function CommandList({
className,
onScroll,
children,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
const [progress, setProgress] = React.useState(0);
return (
<div className="relative flex min-h-0 flex-1 flex-col">
<CommandPrimitive.List
data-slot="command-list"
onScroll={(event) => {
setProgress(
Math.min(1, Math.max(0, event.currentTarget.scrollTop / 24))
);
onScroll?.(event);
}}
className={cn(
"min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-contain p-2 outline-none [scrollbar-width:thin]",
className
)}
{...props}
>
{children}
</CommandPrimitive.List>
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 top-0 z-10 h-6"
>
<div
className="absolute inset-0 backdrop-blur-[1px] [mask-image:linear-gradient(to_bottom,black,transparent)]"
style={{ opacity: progress }}
/>
<div
className="absolute inset-x-0 top-0 h-4 backdrop-blur-[4px] [mask-image:linear-gradient(to_bottom,black,transparent)]"
style={{ opacity: progress }}
/>
<div
className="absolute inset-0 bg-linear-to-b from-[var(--surface-card,var(--popover))] to-transparent"
style={{ opacity: progress }}
/>
</div>
</div>
);
}
function CommandEmpty({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className={cn(
"flex min-h-32 flex-col items-center justify-center gap-1 px-4 text-center text-sm font-medium text-[color:var(--command-muted-foreground,var(--muted-foreground))]",
className
)}
{...props}
/>
);
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
// cmdk nests the rows in a [cmdk-group-items] box, so the 2px rhythm
// between rows is set there and the 8px between sections on the group.
"flex flex-col gap-0.5 p-0 text-[color:var(--foreground-strong,var(--popover-foreground))] [&:not(:first-child)]:pt-2 [&>[cmdk-group-items]]:flex [&>[cmdk-group-items]]:flex-col [&>[cmdk-group-items]]:gap-0.5 **:[[cmdk-group-heading]]:px-3 **:[[cmdk-group-heading]]:pt-1 **:[[cmdk-group-heading]]:pb-1 **:[[cmdk-group-heading]]:text-[13px] **:[[cmdk-group-heading]]:leading-[18px] **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-[color:var(--command-muted-foreground,var(--muted-foreground))]",
className
)}
{...props}
/>
);
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn(
"h-px w-auto bg-[var(--border-subtle,var(--border))]",
className
)}
{...props}
/>
);
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
// oxlint-disable-next-line cobalt/no-transition-on-state-fill -- cmdk selection follows the pointer and arrow keys like a hover row (100ms ease-out); nothing clears it on close
"group/command-item relative flex w-full cursor-pointer items-center gap-2 rounded-[12px] px-3 py-2.5 text-start text-sm font-medium outline-hidden transition-colors duration-100 ease-out select-none data-selected:bg-[var(--surface-sunken,var(--muted))] data-[disabled=true]:pointer-events-none data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50 motion-reduce:transition-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:text-[color:var(--icon-foreground,var(--muted-foreground))] [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
);
}
// A pill that looks like a search field and opens the palette: the search
// icon and label on the left, the shortcut as a kbd chip on the right.
function CommandTrigger({
className,
children,
shortcut,
...props
}: React.ComponentProps<typeof Button> & { shortcut?: React.ReactNode }) {
return (
<Button
data-slot="command-trigger"
variant="secondary"
shape="pill"
className={cn(
// Secondary is the sidebar colour in light, so the pill would vanish.
// --surface-strong is the filled step in both themes.
"h-9 w-full justify-between bg-[var(--surface-strong,var(--secondary))] font-normal text-muted-foreground",
className
)}
{...props}
>
<span className="inline-flex items-center gap-2">
<RiSearchLine className="size-5" />
{children}
</span>
{/* A key hint means nothing on a phone, so it is hidden below sm. */}
{shortcut ? <Kbd className="max-sm:hidden">{shortcut}</Kbd> : null}
</Button>
);
}
// A bar under the list inside CommandDialog, for key hints.
function CommandFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="command-footer"
className={cn(
"flex h-10 shrink-0 items-center gap-2 overflow-hidden border-t border-[color:var(--border-subtle,var(--border))] px-4 text-xs font-medium text-[color:var(--command-muted-foreground,var(--muted-foreground))]",
className
)}
{...props}
/>
);
}
function CommandShortcut({ className, ...props }: React.ComponentProps<"kbd">) {
return (
<Kbd
data-slot="command-shortcut"
className={cn("ml-auto tracking-[0.15px]", className)}
{...props}
/>
);
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
CommandTrigger,
CommandFooter,
};