For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to documentation URLs. Homepage markdown is available at /llms.md/content.md. An agent skill is available at /.well-known/agent-skills/site-skill.md.

Command

A searchable list of actions and destinations, built on cmdk and Base UI.

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.

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,
};