Revola

Command

Fast, composable, unstyled & responsive command menu for React.

Overview

Revola's Command Palette is a responsive drop-in replacement for shadcn/ui's command component. Built on cmdk, it automatically switches between modal dialog on desktop and drawer on mobile.

Adapting it to your existing command setup is as simple as updating the import path and renaming the component.

Installation

Install via Shadcn CLI

If you're already using shadcn in your project, you can install Revola with:

npx shadcn@latest add https://revola.sameerjs.com/r/responsive-command.json

Manual Installation

Install dependencies

Install the required packages:

npm install cmdk

Add the component

Copy and paste the following code into your project:

responsive-command.tsx
"use client";
 
import * as React from "react";
import { Command as CommandPrimitive } from "cmdk";
 
import { SearchIcon } from "lucide-react";
 
import {
  ResponsiveDialog,
  ResponsiveDialogContent,
  ResponsiveDialogDescription,
  ResponsiveDialogHeader,
  ResponsiveDialogTitle,
  useResponsiveDialog,
} from "@/components/ui/revola";
import { cn } from "@/lib/utils";
 
const ResponsiveCommand = React.forwardRef<
  React.ComponentRef<typeof CommandPrimitive>,
  React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => {
  return (
    <CommandPrimitive
      ref={ref}
      className={cn("flex size-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", className)}
      {...props}
    />
  );
});
ResponsiveCommand.displayName = CommandPrimitive.displayName;
 
const ResponsiveCommandDialog = ({
  title = "Command Palette",
  description = "Search for a command to run...",
  children,
  className,
  showCloseButton = true,
  ...props
}: React.ComponentPropsWithoutRef<typeof ResponsiveDialog> & {
  title?: string;
  description?: string;
  className?: string;
  showCloseButton?: boolean;
}) => {
  return (
    <ResponsiveDialog shouldScaleBackground={false} {...props}>
      <ResponsiveDialogContent
        showCloseButton={showCloseButton}
        className={cn("mx-auto overflow-hidden bg-popover sm:max-w-lg [&>button:last-child]:hidden", className)}
      >
        <ResponsiveDialogHeader className="sr-only">
          <ResponsiveDialogTitle>{title}</ResponsiveDialogTitle>
          <ResponsiveDialogDescription>{description}</ResponsiveDialogDescription>
        </ResponsiveDialogHeader>
        <ResponsiveCommand className="max-h-[100svh] [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-3 [&_[cmdk-item]]:py-2">
          {children}
        </ResponsiveCommand>
      </ResponsiveDialogContent>
    </ResponsiveDialog>
  );
};
ResponsiveCommandDialog.displayName = "ResponsiveCommandDialog";
 
const ResponsiveCommandInput = React.forwardRef<
  React.ComponentRef<typeof CommandPrimitive.Input>,
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => {
  return (
    <div className="flex items-center border-b border-input px-5" cmdk-input-wrapper="">
      <SearchIcon size={20} className="me-3 text-muted-foreground/80" />
      <CommandPrimitive.Input
        ref={ref}
        className={cn(
          "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground/70 disabled:cursor-not-allowed disabled:opacity-50",
          className
        )}
        {...props}
      />
    </div>
  );
});
ResponsiveCommandInput.displayName = CommandPrimitive.Input.displayName;
 
const ResponsiveCommandList = React.forwardRef<
  React.ComponentRef<typeof CommandPrimitive.List>,
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => {
  let direction: "top" | "bottom" | "left" | "right" | undefined;
  let onlyDialog = false;
 
  try {
    const context = useResponsiveDialog();
    direction = context.direction;
    onlyDialog = context.onlyDialog || false;
  } catch {
    direction = undefined;
    onlyDialog = false;
  }
 
  return (
    <CommandPrimitive.List
      ref={ref}
      className={cn(
        "flex-1 overflow-y-auto overflow-x-hidden sm:max-h-[320px]",
        direction && "max-h-[calc(100svh-5rem)]",
        onlyDialog && "max-h-[320px]",
        className
      )}
      {...props}
    />
  );
});
ResponsiveCommandList.displayName = CommandPrimitive.List.displayName;
 
const ResponsiveCommandLoading = React.forwardRef<
  React.ComponentRef<typeof CommandPrimitive.Loading>,
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Loading>
>(({ className, ...props }, ref) => (
  <CommandPrimitive.Loading ref={ref} className={cn("py-6 text-center text-sm", className)} {...props} />
));
 
ResponsiveCommandLoading.displayName = CommandPrimitive.Loading.displayName;
 
const ResponsiveCommandEmpty = React.forwardRef<
  React.ComponentRef<typeof CommandPrimitive.Empty>,
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>(({ ...props }, ref) => {
  return <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />;
});
ResponsiveCommandEmpty.displayName = CommandPrimitive.Empty.displayName;
 
const ResponsiveCommandGroup = React.forwardRef<
  React.ComponentRef<typeof CommandPrimitive.Group>,
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => {
  return (
    <CommandPrimitive.Group
      className={cn(
        "overflow-hidden p-2 text-foreground [&_[cmdk-group-heading]]:px-3 [&_[cmdk-group-heading]]:py-2 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
        className
      )}
      {...props}
    />
  );
});
ResponsiveCommandGroup.displayName = CommandPrimitive.Group.displayName;
 
const ResponsiveCommandSeparator = React.forwardRef<
  React.ComponentRef<typeof CommandPrimitive.Separator>,
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => {
  return <CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />;
});
ResponsiveCommandSeparator.displayName = CommandPrimitive.Separator.displayName;
 
const ResponsiveCommandItem = React.forwardRef<
  React.ComponentRef<typeof CommandPrimitive.Item>,
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => {
  return (
    <CommandPrimitive.Item
      ref={ref}
      className={cn(
        "relative flex cursor-default select-none items-center gap-3 rounded-md px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0",
        className
      )}
      {...props}
    />
  );
});
ResponsiveCommandItem.displayName = CommandPrimitive.Item.displayName;
 
const ResponsiveCommandShortcut = ({ className, ...props }: React.ComponentProps<"kbd">) => {
  return (
    <kbd
      className={cn(
        "-me-1 ms-auto inline-flex h-5 max-h-full items-center rounded border bg-background px-1 font-[inherit] text-[0.625rem] font-medium text-muted-foreground/70",
        className
      )}
      {...props}
    />
  );
};
 
export {
  ResponsiveCommand,
  ResponsiveCommandDialog,
  ResponsiveCommandEmpty,
  ResponsiveCommandGroup,
  ResponsiveCommandInput,
  ResponsiveCommandItem,
  ResponsiveCommandList,
  ResponsiveCommandLoading,
  ResponsiveCommandSeparator,
  ResponsiveCommandShortcut,
};

Usage

import {
  ResponsiveCommandDialog,
  ResponsiveCommandEmpty,
  ResponsiveCommandGroup,
  ResponsiveCommandInput,
  ResponsiveCommandItem,
  ResponsiveCommandList,
  ResponsiveCommandSeparator,
  ResponsiveCommandShortcut,
} from "@/components/ui/responsive-command"
<ResponsiveCommandDialog>
  <ResponsiveCommandInput placeholder="Type a command or search..." />
  <ResponsiveCommandList>
    <ResponsiveCommandEmpty>No results found.</ResponsiveCommandEmpty>
    <ResponsiveCommandGroup heading="Suggestions">
      <ResponsiveCommandItem>Calendar</ResponsiveCommandItem>
      <ResponsiveCommandItem>Search Emoji</ResponsiveCommandItem>
      <ResponsiveCommandItem>Calculator</ResponsiveCommandItem>
    </ResponsiveCommandGroup>
    <ResponsiveCommandSeparator />
    <ResponsiveCommandGroup heading="Settings">
      <ResponsiveCommandItem>Profile</ResponsiveCommandItem>
      <ResponsiveCommandItem>Billing</ResponsiveCommandItem>
      <ResponsiveCommandItem>Settings</ResponsiveCommandItem>
    </ResponsiveCommandGroup>
  </ResponsiveCommandList>
</ResponsiveCommandDialog>

Examples

Combobox

A searchable combobox for selecting frameworks. Shows the drop-in replacement nature of ResponsiveCommand. Adapted from shadcn/ui.

On this page