Command Palette

Search for a command...

Combobox

Autocomplete input and command palette with a list of suggestions.

"use client";

import * as React from "react";
import { LuCheck, LuChevronsUpDown } from "react-icons/lu";
import { css } from "styled-system/css";
import { Button } from "@/components/ui/button";
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";

const frameworks = [
  { value: "next.js", label: "Next.js" },
  { value: "sveltekit", label: "SvelteKit" },
  { value: "nuxt.js", label: "Nuxt.js" },
  { value: "remix", label: "Remix" },
  { value: "astro", label: "Astro" },
];

export default function ComboboxDemo() {
  const [open, setOpen] = React.useState(false);
  const [value, setValue] = React.useState("");

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          role="combobox"
          aria-expanded={open}
          css={{ w: "200px", justifyContent: "space-between" }}
        >
          {value
            ? frameworks.find((framework) => framework.value === value)?.label
            : "Select framework..."}
          <LuChevronsUpDown opacity="0.5" />
        </Button>
      </PopoverTrigger>
      <PopoverContent css={{ w: "200px", p: "0" }}>
        <Command>
          <CommandInput placeholder="Search framework..." />
          <CommandList>
            <CommandEmpty>No framework found.</CommandEmpty>
            <CommandGroup>
              {frameworks.map((framework) => (
                <CommandItem
                  key={framework.value}
                  value={framework.value}
                  onSelect={(currentValue) => {
                    setValue(currentValue === value ? "" : currentValue);
                    setOpen(false);
                  }}
                >
                  {framework.label}
                  <LuCheck
                    className={css({
                      ml: "auto",
                      opacity: value === framework.value ? "1" : "0",
                    })}
                  />
                </CommandItem>
              ))}
            </CommandGroup>
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  );
}

Installation

The Combobox is built using a composition of the <Popover /> and the <Command /> components.

See installation instructions for the Popover and the Command components.

Usage

"use client";

import * as React from "react";
import { LuCheck, LuChevronsUpDown } from "react-icons/lu";
import { css, cx } from "styled-system/css";
import { Button } from "@/components/ui/button";
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";

const frameworks = [
  {
    value: "next.js",
    label: "Next.js",
  },
  {
    value: "sveltekit",
    label: "SvelteKit",
  },
  {
    value: "nuxt.js",
    label: "Nuxt.js",
  },
  {
    value: "remix",
    label: "Remix",
  },
  {
    value: "astro",
    label: "Astro",
  },
];

export default function ComboboxDemo() {
  const [open, setOpen] = React.useState(false);
  const [value, setValue] = React.useState("");

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          role="combobox"
          aria-expanded={open}
          css={{ w: "200px", justifyContent: "space-between" }}
        >
          {value
            ? frameworks.find((framework) => framework.value === value)?.label
            : "Select framework..."}
          <LuChevronsUpDown className={css({ opacity: "0.5" })} />
        </Button>
      </PopoverTrigger>
      <PopoverContent css={{ w: "200px", p: "0" }}>
        <Command>
          <CommandInput placeholder="Search framework..." />
          <CommandList>
            <CommandEmpty>No framework found.</CommandEmpty>
            <CommandGroup>
              {frameworks.map((framework) => (
                <CommandItem
                  key={framework.value}
                  value={framework.value}
                  onSelect={(currentValue) => {
                    setValue(currentValue === value ? "" : currentValue);
                    setOpen(false);
                  }}
                >
                  {framework.label}
                  <LuCheck
                    className={cx(
                      css({ ml: "auto" }),
                      value === framework.value ? css({ opacity: "1" }) : css({ opacity: "0" })
                    )}
                  />
                </CommandItem>
              ))}
            </CommandGroup>
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  );
}

Examples

Popover

Status

"use client";

import * as React from "react";
import { LuCircle, LuCircleArrowUp, LuCircleCheck, LuCircleHelp, LuCircleX } from "react-icons/lu";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";

type Status = {
  value: string;
  label: string;
  icon: React.ElementType;
};

const statuses: Status[] = [
  { value: "backlog", label: "Backlog", icon: LuCircleHelp },
  { value: "todo", label: "Todo", icon: LuCircle },
  { value: "in progress", label: "In Progress", icon: LuCircleArrowUp },
  { value: "done", label: "Done", icon: LuCircleCheck },
  { value: "canceled", label: "Canceled", icon: LuCircleX },
];

export default function ComboboxPopover() {
  const [open, setOpen] = React.useState(false);
  const [selectedStatus, setSelectedStatus] = React.useState<Status | null>(null);

  return (
    <styled.div css={{ display: "flex", alignItems: "center", gap: "4" }}>
      <styled.p css={{ textStyle: "sm", color: "muted.fg" }}>Status</styled.p>
      <Popover open={open} onOpenChange={setOpen}>
        <PopoverTrigger asChild>
          <Button
            variant="outline"
            size="sm"
            css={{ width: "150px", justifyContent: "flex-start" }}
          >
            {selectedStatus ? (
              <>
                <selectedStatus.icon
                  className={css({ marginRight: "2", width: "4", height: "4", flexShrink: "0" })}
                />
                {selectedStatus.label}
              </>
            ) : (
              "+ Set status"
            )}
          </Button>
        </PopoverTrigger>
        <PopoverContent side="right" align="start" css={{ p: "0" }}>
          <Command>
            <CommandInput placeholder="Change status..." />
            <CommandList>
              <CommandEmpty>No results found.</CommandEmpty>
              <CommandGroup>
                {statuses.map((status) => (
                  <CommandItem
                    key={status.value}
                    value={status.value}
                    onSelect={(value) => {
                      setSelectedStatus(statuses.find((s) => s.value === value) || null);
                      setOpen(false);
                    }}
                  >
                    <status.icon
                      className={css({
                        marginRight: "2",
                        width: "4",
                        height: "4",
                        opacity: status.value === selectedStatus?.value ? "1" : "0.4",
                      })}
                    />
                    <span>{status.label}</span>
                  </CommandItem>
                ))}
              </CommandGroup>
            </CommandList>
          </Command>
        </PopoverContent>
      </Popover>
    </styled.div>
  );
}

This is the language that will be used in the dashboard.

"use client";

import { Controller, useForm } from "react-hook-form";
import { LuCheck, LuChevronsUpDown } from "react-icons/lu";
import { zodResolver } from "@hookform/resolvers/zod";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
import { z } from "zod";
import { toast } from "@/hooks/use-toast";
import { Button } from "@/components/ui/button";
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command";
import { Field, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";

const languages = [
  { label: "English", value: "en" },
  { label: "French", value: "fr" },
  { label: "German", value: "de" },
  { label: "Spanish", value: "es" },
  { label: "Portuguese", value: "pt" },
  { label: "Russian", value: "ru" },
  { label: "Japanese", value: "ja" },
  { label: "Korean", value: "ko" },
  { label: "Chinese", value: "zh" },
] as const;

const formSchema = z.object({
  language: z.string({
    required_error: "Please select a language.",
  }),
});

type FormSchema = z.infer<typeof formSchema>;

export default function ComboboxForm() {
  const form = useForm<FormSchema>({
    resolver: zodResolver(formSchema),
  });

  const onSubmit = form.handleSubmit((data) => {
    toast({
      title: "You submitted the following values:",
      description: (
        <styled.pre
          css={{ mt: "2", w: "340px", rounded: "md", bg: "slate.950", p: "4", borderWidth: "1px" }}
        >
          <styled.code css={{ color: "white" }}>{JSON.stringify(data, null, 2)}</styled.code>
        </styled.pre>
      ),
    });
  });

  return (
    <styled.form onSubmit={onSubmit} css={{ spaceY: "6" }}>
      <Controller
        control={form.control}
        name="language"
        render={({ field, fieldState }) => (
          <Field data-invalid={fieldState.invalid} css={{ display: "flex", flexDir: "column" }}>
            <FieldLabel>Language</FieldLabel>
            <Popover>
              <PopoverTrigger asChild>
                <Button
                  variant="outline"
                  role="combobox"
                  css={{
                    w: "200px",
                    justifyContent: "space-between",
                    color: !field.value ? "muted.fg" : undefined,
                  }}
                >
                  {field.value
                    ? languages.find((language) => language.value === field.value)?.label
                    : "Select language"}
                  <LuChevronsUpDown
                    className={css({
                      ml: "2",
                      w: "4",
                      h: "4",
                      flexShrink: "0",
                      opacity: "0.5",
                    })}
                  />
                </Button>
              </PopoverTrigger>
              <PopoverContent css={{ w: "200px", p: "0" }}>
                <Command>
                  <CommandInput placeholder="Search language..." />
                  <CommandList>
                    <CommandEmpty>No language found.</CommandEmpty>
                    <CommandGroup>
                      {languages.map((language) => (
                        <CommandItem
                          value={language.label}
                          key={language.value}
                          onSelect={() => {
                            form.setValue("language", language.value);
                          }}
                        >
                          {language.label}
                          <LuCheck
                            className={css({
                              ml: "auto",
                              opacity: language.value === field.value ? "1" : "0",
                            })}
                          />
                        </CommandItem>
                      ))}
                    </CommandGroup>
                  </CommandList>
                </Command>
              </PopoverContent>
            </Popover>
            <FieldDescription>
              This is the language that will be used in the dashboard.
            </FieldDescription>
            <FieldError>{fieldState.error?.message}</FieldError>
          </Field>
        )}
      />
      <Button type="submit">Submit</Button>
    </styled.form>
  );
}

Asynchronous

"use client";

import * as React from "react";
import { LuCheck, LuChevronsUpDown, LuLoader } from "react-icons/lu";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";

interface Framework {
  value: string;
  label: string;
}

export default function ComboboxAsync() {
  const [open, setOpen] = React.useState(false);
  const [value, setValue] = React.useState("");
  const [searchQuery, setSearchQuery] = React.useState("");
  const [frameworks, setFrameworks] = React.useState<Framework[]>([]);
  const [isLoading, setIsLoading] = React.useState(false);

  React.useEffect(() => {
    if (open) {
      fetchFrameworks(searchQuery);
    }
  }, [open, searchQuery]);

  const fetchFrameworks = async (query: string) => {
    setIsLoading(true);
    try {
      await new Promise((resolve) => setTimeout(resolve, 1000));

      const mockData = [
        { value: "next.js", label: "Next.js" },
        { value: "sveltekit", label: "SvelteKit" },
        { value: "nuxt.js", label: "Nuxt.js" },
        { value: "remix", label: "Remix" },
        { value: "astro", label: "Astro" },
      ].filter((item) => (query ? item.label.toLowerCase().includes(query.toLowerCase()) : true));

      setFrameworks(mockData);
    } catch (error) {
      console.error("Error fetching frameworks:", error);
    } finally {
      setIsLoading(false);
    }
  };

  const handleInputChange = (value: string) => {
    setSearchQuery(value);
  };

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          role="combobox"
          aria-expanded={open}
          css={{ w: "200px", justifyContent: "space-between" }}
        >
          {value
            ? frameworks.find((framework) => framework.value === value)?.label ||
              "Select framework..."
            : "Select framework..."}
          <LuChevronsUpDown className={css({ opacity: "0.5" })} />
        </Button>
      </PopoverTrigger>
      <PopoverContent css={{ w: "200px", p: "0" }}>
        <Command>
          <CommandInput
            placeholder="Search framework..."
            value={searchQuery}
            onValueChange={handleInputChange}
          />
          <CommandList>
            {isLoading ? (
              <styled.div
                css={{
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                  py: "2",
                  px: "2",
                  color: "gray.500",
                  fontSize: "sm",
                }}
              >
                <LuLoader
                  className={css({
                    animation: "spin 2s linear infinite",
                    mr: "2",
                  })}
                />
                <span>Loading frameworks...</span>
              </styled.div>
            ) : (
              <>
                <CommandEmpty>No framework found.</CommandEmpty>
                <CommandGroup>
                  {frameworks.map((framework) => (
                    <CommandItem
                      key={framework.value}
                      value={framework.value}
                      onSelect={(currentValue) => {
                        setValue(currentValue === value ? "" : currentValue);
                        setOpen(false);
                      }}
                    >
                      {framework.label}
                      <LuCheck
                        className={css({
                          ml: "auto",
                          opacity: value === framework.value ? "1" : "0",
                        })}
                      />
                    </CommandItem>
                  ))}
                </CommandGroup>
              </>
            )}
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  );
}

Form

This is the language that will be used in the dashboard.

"use client";

import { Controller, useForm } from "react-hook-form";
import { LuCheck, LuChevronsUpDown } from "react-icons/lu";
import { zodResolver } from "@hookform/resolvers/zod";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
import { z } from "zod";
import { toast } from "@/hooks/use-toast";
import { Button } from "@/components/ui/button";
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command";
import { Field, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";

const languages = [
  { label: "English", value: "en" },
  { label: "French", value: "fr" },
  { label: "German", value: "de" },
  { label: "Spanish", value: "es" },
  { label: "Portuguese", value: "pt" },
  { label: "Russian", value: "ru" },
  { label: "Japanese", value: "ja" },
  { label: "Korean", value: "ko" },
  { label: "Chinese", value: "zh" },
] as const;

const formSchema = z.object({
  language: z.string({
    required_error: "Please select a language.",
  }),
});

type FormSchema = z.infer<typeof formSchema>;

export default function ComboboxForm() {
  const form = useForm<FormSchema>({
    resolver: zodResolver(formSchema),
  });

  const onSubmit = form.handleSubmit((data) => {
    toast({
      title: "You submitted the following values:",
      description: (
        <styled.pre
          css={{ mt: "2", w: "340px", rounded: "md", bg: "slate.950", p: "4", borderWidth: "1px" }}
        >
          <styled.code css={{ color: "white" }}>{JSON.stringify(data, null, 2)}</styled.code>
        </styled.pre>
      ),
    });
  });

  return (
    <styled.form onSubmit={onSubmit} css={{ spaceY: "6" }}>
      <Controller
        control={form.control}
        name="language"
        render={({ field, fieldState }) => (
          <Field data-invalid={fieldState.invalid} css={{ display: "flex", flexDir: "column" }}>
            <FieldLabel>Language</FieldLabel>
            <Popover>
              <PopoverTrigger asChild>
                <Button
                  variant="outline"
                  role="combobox"
                  css={{
                    w: "200px",
                    justifyContent: "space-between",
                    color: !field.value ? "muted.fg" : undefined,
                  }}
                >
                  {field.value
                    ? languages.find((language) => language.value === field.value)?.label
                    : "Select language"}
                  <LuChevronsUpDown
                    className={css({
                      ml: "2",
                      w: "4",
                      h: "4",
                      flexShrink: "0",
                      opacity: "0.5",
                    })}
                  />
                </Button>
              </PopoverTrigger>
              <PopoverContent css={{ w: "200px", p: "0" }}>
                <Command>
                  <CommandInput placeholder="Search language..." />
                  <CommandList>
                    <CommandEmpty>No language found.</CommandEmpty>
                    <CommandGroup>
                      {languages.map((language) => (
                        <CommandItem
                          value={language.label}
                          key={language.value}
                          onSelect={() => {
                            form.setValue("language", language.value);
                          }}
                        >
                          {language.label}
                          <LuCheck
                            className={css({
                              ml: "auto",
                              opacity: language.value === field.value ? "1" : "0",
                            })}
                          />
                        </CommandItem>
                      ))}
                    </CommandGroup>
                  </CommandList>
                </Command>
              </PopoverContent>
            </Popover>
            <FieldDescription>
              This is the language that will be used in the dashboard.
            </FieldDescription>
            <FieldError>{fieldState.error?.message}</FieldError>
          </Field>
        )}
      />
      <Button type="submit">Submit</Button>
    </styled.form>
  );
}

Multi Select

"use client";

import * as React from "react";
import { LuCheck, LuChevronsUpDown } from "react-icons/lu";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";

const frameworks = [
  { value: "next.js", label: "Next.js" },
  { value: "sveltekit", label: "SvelteKit" },
  { value: "nuxt.js", label: "Nuxt.js" },
  { value: "remix", label: "Remix" },
  { value: "astro", label: "Astro" },
];

type Framework = (typeof frameworks)[number];

export default function ComboboxMulti() {
  const [open, setOpen] = React.useState(false);
  const [selectedFrameworks, setSelectedFrameworks] = React.useState<Framework[]>([]);

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          role="combobox"
          aria-expanded={open}
          css={{ w: "fit", minW: "280px", justifyContent: "space-between" }}
        >
          {selectedFrameworks.length > 0
            ? selectedFrameworks.map((framework) => framework.label).join(", ")
            : "Select framework..."}
          <LuChevronsUpDown className={css({ color: "muted.fg" })} />
        </Button>
      </PopoverTrigger>
      <PopoverContent align="start" css={{ w: "300px", p: "0" }}>
        <Command>
          <CommandInput placeholder="Search framework..." />
          <CommandList>
            <CommandEmpty>No framework found.</CommandEmpty>
            <CommandGroup>
              {frameworks.map((framework) => (
                <CommandItem
                  key={framework.value}
                  value={framework.value}
                  onSelect={(currentValue) => {
                    setSelectedFrameworks(
                      selectedFrameworks.some((f) => f.value === currentValue)
                        ? selectedFrameworks.filter((f) => f.value !== currentValue)
                        : [...selectedFrameworks, framework]
                    );
                  }}
                >
                  <styled.div
                    data-selected={selectedFrameworks.some((f) => f.value === framework.value)}
                    css={{
                      borderWidth: "1px",
                      borderColor: "input",
                      pointerEvents: "none",
                      w: "4",
                      h: "4",
                      flexShrink: "0",
                      rounded: "4px",
                      transition: "all",
                      userSelect: "none",
                      "& > svg": { opacity: "0" },
                      "&[data-selected=true]": {
                        borderColor: "primary",
                        bg: "primary",
                        color: "primary.fg",
                        "& > svg": { opacity: "1" },
                      },
                    }}
                  >
                    <LuCheck className={css({ w: "3.5", h: "3.5", color: "current" })} />
                  </styled.div>
                  {framework.label}
                </CommandItem>
              ))}
            </CommandGroup>
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  );
}