Kosorikōsori alpha

Command

Fast, composable, unstyled command menu for React.

Dependencies

About

The <Command /> component uses the cmdk component by pacocoursey.

Installation

Install the primitive

Install the cmdk package.

npm install cmdk

Copy-paste the component

Copy and paste the component code in a .tsx file.

import type { DialogProps } from '@radix-ui/react-dialog';
import { forwardRef } from 'react';
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
import { clsx } from 'clsx/lite';
import {
  Command as CMDK,
  CommandEmpty as CMDKEmpty,
  CommandGroup as CMDKGroup,
  CommandInput as CMDKInput,
  CommandItem as CMDKItem,
  CommandList as CMDKList,
  CommandSeparator as CMDKSeparator,
} from 'cmdk';
import { tv } from 'tailwind-variants';
 
import { Dialog, DialogContent } from '@kosori/ui/dialog';
 
const commandStyles = tv({
  slots: {
    base: 'flex h-full w-full flex-col overflow-hidden rounded-xl bg-grey-base',
    dialogContent: 'overflow-hidden p-0 shadow-lg',
    dialogCommand: clsx(
      '[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:text-grey-text',
      '[&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0',
      '[&_[cmdk-group]]:px-1',
      '[&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5',
      '[&_[cmdk-input]]:h-11',
      '[&_[cmdk-item]]:h-auto [&_[cmdk-item]]:px-3 [&_[cmdk-item]]:py-2',
      '[&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5',
    ),
    inputWrapper:
      'flex items-center border-b border-grey-line px-3 outline-none',
    inputIcon: 'mr-2 h-4 w-4 shrink-0 text-grey-text',
    input: clsx(
      'flex h-10 w-full rounded-lg bg-grey-base py-3 text-sm',
      'placeholder:text-grey-placeholder-text',
      'focus:outline-none',
    ),
    list: clsx(
      'max-h-xs overflow-h-auto h-full overflow-x-hidden outline-none',
      'focus-visible:ring-4 focus-visible:ring-primary-focus-ring',
    ),
    empty: 'py-6 text-center text-sm',
    group: clsx(
      'overflow-hidden p-1 text-grey-text-contrast',
      '[&_[cmdk-group-heading]]:flex [&_[cmdk-group-heading]]:select-none [&_[cmdk-group-heading]]:items-center [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-grey-text',
    ),
    item: clsx(
      'group relative flex cursor-pointer select-none items-center rounded-md px-2 py-1.5 text-sm outline-none',
      'aria-selected:bg-primary-bg-hover',
      'aria-disabled:cursor-not-allowed aria-disabled:text-grey-solid',
    ),
    shortcut: clsx(
      'ml-auto text-xs tracking-widest text-grey-text',
      'group-aria-disabled:text-grey-solid',
    ),
    separator: '-mx-1 h-px bg-grey-line',
  },
});
 
const {
  base,
  dialogContent,
  dialogCommand,
  inputWrapper,
  inputIcon,
  input,
  list,
  empty,
  group,
  item,
  shortcut,
  separator,
} = commandStyles();
 
/**
 * Command component that provides a command palette for executing commands or searching.
 *
 * @param {CommandProps} props - The props for the Command component.
 *
 * @example
 * <Command>
 *   <CommandInput placeholder='Type a command or search...' />
 *   <CommandList>
 *     <CommandEmpty>No results found.</CommandEmpty>
 *     <CommandGroup heading='Suggestions'>
 *       <CommandItem>Calendar</CommandItem>
 *       <CommandItem>Search Emoji</CommandItem>
 *       <CommandItem>Calculator</CommandItem>
 *     </CommandGroup>
 *     <CommandSeparator />
 *     <CommandGroup heading='Settings'>
 *       <CommandItem>Profile</CommandItem>
 *       <CommandItem>Billing</CommandItem>
 *       <CommandItem>Settings</CommandItem>
 *     </CommandGroup>
 *   </CommandList>
 * </Command>
 *
 * @see {@link https://dub.sh/ui-command Command Docs} for further information.
 */
export const Command = forwardRef<
  React.ElementRef<typeof CMDK>,
  React.ComponentPropsWithoutRef<typeof CMDK>
>(({ className, ...props }, ref) => (
  <CMDK ref={ref} className={base({ className })} {...props} />
));
 
Command.displayName = CMDK.displayName;
 
/**
 * CommandDialog component that wraps the Command component in a dialog.
 *
 * @param {DialogProps} props - The props for the CommandDialog component.
 *
 * @example
 * <CommandDialog>
 *   <Command>...</Command>
 * </CommandDialog>
 */
export const CommandDialog = ({ children, ...props }: DialogProps) => {
  return (
    <Dialog {...props}>
      <DialogContent className={dialogContent()}>
        <Command className={dialogCommand()}>{children}</Command>
      </DialogContent>
    </Dialog>
  );
};
 
type CommandInputRef = React.ElementRef<typeof CMDKInput>;
type CommandInputProps = React.ComponentPropsWithoutRef<typeof CMDKInput>;
 
/**
 * CommandInput component that allows users to input commands or search terms.
 *
 * @param {CommandInputProps} props - The props for the CommandInput component.
 *
 * @example
 * <CommandInput placeholder='Type a command or search...' />
 */
export const CommandInput = forwardRef<CommandInputRef, CommandInputProps>(
  ({ className, ...props }, ref) => (
    <div className={inputWrapper()} cmdk-input-wrapper=''>
      <MagnifyingGlassIcon className={inputIcon()} />
      <CMDKInput ref={ref} className={input({ className })} {...props} />
    </div>
  ),
);
 
CommandInput.displayName = CMDKInput.displayName;
 
type CommandListRef = React.ElementRef<typeof CMDKList>;
type CommandListProps = React.ComponentPropsWithoutRef<typeof CMDKList>;
 
/**
 * CommandList component that wraps the list of command items.
 *
 * @param {CommandListProps} props - The props for the CommandList component.
 *
 * @example
 * <CommandList>
 *   <CommandItem>...</CommandItem>
 * </CommandList>
 */
export const CommandList = forwardRef<CommandListRef, CommandListProps>(
  ({ className, ...props }, ref) => (
    <CMDKList ref={ref} className={list({ className })} {...props} />
  ),
);
 
CommandList.displayName = CMDKList.displayName;
 
type CommandEmptyRef = React.ElementRef<typeof CMDKEmpty>;
type CommandEmptyProps = React.ComponentPropsWithoutRef<typeof CMDKEmpty>;
 
/**
 * CommandEmpty component that displays a message when no results are found.
 *
 * @param {CommandEmptyProps} props - The props for the CommandEmpty component.
 *
 * @example
 * <CommandEmpty>No results found.</CommandEmpty>
 */
export const CommandEmpty = forwardRef<CommandEmptyRef, CommandEmptyProps>(
  ({ className, ...props }, ref) => (
    <CMDKEmpty ref={ref} className={empty({ className })} {...props} />
  ),
);
 
CommandEmpty.displayName = CMDKEmpty.displayName;
 
type CommandGroupRef = React.ElementRef<typeof CMDKGroup>;
type CommandGroupProps = React.ComponentPropsWithoutRef<typeof CMDKGroup>;
 
/**
 * CommandGroup component that groups related command items.
 *
 * @param {CommandGroupProps} props - The props for the CommandGroup component.
 *
 * @example
 * <CommandGroup heading='Suggestions'>
 *   <CommandItem>...</CommandItem>
 * </CommandGroup>
 */
export const CommandGroup = forwardRef<CommandGroupRef, CommandGroupProps>(
  ({ className, ...props }, ref) => (
    <CMDKGroup ref={ref} className={group({ className })} {...props} />
  ),
);
 
CommandGroup.displayName = CMDKGroup.displayName;
 
type CommandItemRef = React.ElementRef<typeof CMDKItem>;
type CommandItemProps = React.ComponentPropsWithoutRef<typeof CMDKItem>;
 
/**
 * CommandItem component that represents a single item in the command list.
 *
 * @param {CommandItemProps} props - The props for the CommandItem component.
 *
 * @example
 * <CommandItem>...</CommandItem>
 */
export const CommandItem = forwardRef<CommandItemRef, CommandItemProps>(
  ({ className, ...props }, ref) => (
    <CMDKItem ref={ref} className={item({ className })} {...props} />
  ),
);
 
CommandItem.displayName = CMDKItem.displayName;
 
type CommandShortcutProps = React.HTMLAttributes<HTMLSpanElement>;
 
/**
 * CommandShortcut component that displays a keyboard shortcut for a command item.
 *
 * @param {CommandShortcutProps} props - The props for the CommandShortcut component.
 *
 * @example
 * <CommandShortcut>⌘K</CommandShortcut>
 */
export const CommandShortcut = ({
  className,
  ...props
}: CommandShortcutProps) => {
  return <span className={shortcut({ className })} {...props} />;
};
 
type CommandSeparatorRef = React.ElementRef<typeof CMDKSeparator>;
type CommandSeparatorProps = React.ComponentPropsWithoutRef<
  typeof CMDKSeparator
>;
 
/**
 * CommandSeparator component that visually separates command items.
 *
 * @param {CommandSeparatorProps} props - The props for the CommandSeparator component.
 *
 * @example
 * <CommandSeparator />
 */
export const CommandSeparator = forwardRef<
  CommandSeparatorRef,
  CommandSeparatorProps
>(({ className, ...props }, ref) => (
  <CMDKSeparator ref={ref} className={separator({ className })} {...props} />
));
 
CommandSeparator.displayName = CMDKSeparator.displayName;

Update import paths

Update the @kosori/ui import paths to fit your project structure, for example, using ~/components/ui.

Usage

import {
  Command,
  CommandDialog,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
  CommandShortcut,
} from '~/components/ui/command';
<Command>
  <CommandInput placeholder='Type a command or search...' />
  <CommandList>
    <CommandEmpty>No results found.</CommandEmpty>
    <CommandGroup heading='Suggestions'>
      <CommandItem>Calendar</CommandItem>
      <CommandItem>Search Emoji</CommandItem>
      <CommandItem>Calculator</CommandItem>
    </CommandGroup>
    <CommandSeparator />
    <CommandGroup heading='Settings'>
      <CommandItem>Profile</CommandItem>
      <CommandItem>Billing</CommandItem>
      <CommandItem>Settings</CommandItem>
    </CommandGroup>
  </CommandList>
</Command>

Examples

Dialog

Press J

To show the command menu in a dialog, use the <CommandDialog /> component.

export const CommandMenu = () => {
  const [open, setOpen] = useState(false); 
 
  useEffect(() => {
    const down = (e: KeyboardEvent) => {
      if (e.key === 'j' && (e.metaKey || e.ctrlKey)) {
        e.preventDefault();
        setOpen((open) => !open);
      }
    };
 
    document.addEventListener('keydown', down);
    return () => document.removeEventListener('keydown', down);
  }, []);
 
  return (

    <CommandDialog open={open} onOpenChange={setOpen}>
      <CommandInput placeholder='Type a command or search...' />
      <CommandList>
        <CommandEmpty>No results found.</CommandEmpty>
        <CommandGroup heading='Suggestions'>
          <CommandItem>Calendar</CommandItem>
          <CommandItem>Search Emoji</CommandItem>
          <CommandItem>Calculator</CommandItem>
        </CommandGroup>
      </CommandList>
    </CommandDialog> 
  );
};

Combobox

You can use the <Command /> component as a combobox. See the Combobox page for more information.

On this page