Back to React Spectrum

Command Palette

packages/react-aria-components/docs/examples/command-palette.mdx

2022-12-167.6 KB
Original Source

{/* Copyright 2025 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */}

import {ExampleLayout} from '@react-spectrum/docs'; export default ExampleLayout;

import docs from 'docs:react-aria-components'; import styles from '@react-spectrum/docs/src/docs.css'; import Menu from '@react-spectrum/docs/pages/assets/component-illustrations/Menu.svg'; import TextField from '@react-spectrum/docs/pages/assets/component-illustrations/TextField.svg'; import {ExampleCard} from '@react-spectrum/docs/src/ExampleCard'; import ChevronRight from '@spectrum-icons/workflow/ChevronRight';


keywords: [example, autocomplete, menu, aria, accessibility, react, component] type: component image: command-palette.png description: A command palette with actions, styled with Tailwind CSS.

Command Palette

A Command Palette is an interface that allows users to quickly run commands or navigate to content within an application.

Example

This example uses the Autocomplete component from React Aria Components to filter a list of commands and display them in a Menu. The TextField is used to capture user input and filter the list of available commands.

tsx
import './tailwind.global.css';
tsx
import {
  Autocomplete,
  TextField,
  Menu,
  MenuItem,
  useFilter,
  Input,
  Dialog,
  DialogTrigger,
  Modal,
  ModalOverlay,
  Button
} from 'react-aria-components';
import {useEffect, useMemo, useState} from 'react';

function CommandPaletteExample() {
  let commands = [
    {id: 'new-file', label: 'Create new file…'},
    {id: 'new-folder', label: 'Create new folder…'},
    {id: 'assign', label: 'Assign to…'},
    {id: 'assign-me', label: 'Assign to me'},
    {id: 'status', label: 'Change status…'},
    {id: 'priority', label: 'Change priority…'},
    {id: 'label-add', label: 'Add label…'},
    {id: 'label-remove', label: 'Remove label…'}
  ];

  let [isOpen, setOpen] = useState(false);
  let {contains} = useFilter({sensitivity: 'base'});
  let isMac = useMemo(
    () =>
      typeof navigator === 'undefined'
        ? false
        : /mac(os|intosh)/i.test(navigator.userAgent),
    []
  )

  useEffect(() => {
    const handleKeyDown = (e) => {
      if (e.key === 'k' && (isMac ? e.metaKey : e.ctrlKey)) {
        e.preventDefault();
        setOpen((prev) => !prev);
      } else if (e.key === 'Escape') {
        e.preventDefault();
        setOpen(false);
      }
    };

    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  })

  return (
    <div className="bg-linear-to-r from-indigo-500 to-violet-500 p-4 sm:p-8 h-[340px] rounded-lg flex items-center justify-center">
      <DialogTrigger isOpen={isOpen} onOpenChange={setOpen}>
        <Button className="inline-flex items-center justify-center rounded-xl bg-black/20 bg-clip-padding border border-white/20 px-3 py-2 font-medium font-[inherit] text-sm sm:text-base text-white hover:bg-black/30 pressed:bg-black/40 transition-colors cursor-default outline-hidden focus-visible:ring-2 focus-visible:ring-white/75">
          <span className="block sm:hidden">Tap to open</span>
          <span className="hidden sm:block">
            Type <kbd className="px-2 py-1 m-1 text-xs font-semibold border border-gray-200 rounded-lg">{isMac ? '⌘' : 'Ctrl'}</kbd> + <kbd className="px-2 py-1 m-1 text-xs font-semibold border border-gray-200 rounded-lg">K</kbd> or press here to open
          </span>
        </Button>
        <ModalOverlay
          isDismissable
          className={({ isEntering, isExiting }) => `
          absolute top-0 left-0 w-full h-(--page-height) z-10 bg-black/25
          ${isEntering ? 'animate-in fade-in duration-300 ease-out' : ''}
          ${isExiting ? 'animate-out fade-out duration-200 ease-in' : ''}
        `}
        >
          <div className="sticky top-0 left-0 w-full h-(--visual-viewport-height) flex items-start sm:items-center justify-center p-4 box-border text-center">
            <Modal
              className={({ isEntering, isExiting }) => `
              ${isEntering ? 'animate-in zoom-in-95 ease-out duration-300' : ''}
              ${isExiting ? 'animate-out zoom-out-95 ease-in duration-200' : ''}
            `}
            >
              <Dialog className="outline-hidden relative">
                <div className="flex flex-col gap-1 w-[95vw] sm:w-[500px] max-w-full rounded-xl bg-white shadow-lg p-2">
                  <Autocomplete filter={contains}>
                    <TextField
                      aria-label="Search commands"
                      className="flex flex-col px-3 py-2 rounded-md outline-none placeholder-white/70"
                    >
                      <Input
                        autoFocus
                        placeholder="Search commands…"
                        className="border-none py-2 px-3 leading-5 text-gray-900 bg-transparent outline-hidden text-base focus-visible:ring-2 focus-visible:ring-violet-500 rounded-lg"
                      />
                    </TextField>
                    <Menu
                      items={commands}
                      className="mt-2 p-1 max-h-44 overflow-auto"
                    >
                      {({ label }) => <CommandItem>{label}</CommandItem>}
                    </Menu>
                  </Autocomplete>
                </div>
              </Dialog>
            </Modal>
          </div>
        </ModalOverlay>
      </DialogTrigger>
    </div>
  );
}

function CommandItem(props) {
  return (
    <MenuItem
      {...props}
      className="group flex w-full items-center rounded-md px-3 py-2 box-border outline-none cursor-default text-gray-900 hover:bg-violet-100 pressed:bg-violet-200 focus:bg-violet-500 focus:text-white"
    />
  );
}

Tailwind config

This example uses the following plugins:

When using Tailwind v4, add them to your CSS:

css
@import "tailwindcss";
@plugin "tailwindcss-react-aria-components";
@plugin "tailwindcss-animate";
<details> <summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Tailwind v3</summary>

When using Tailwind v3, add the plugins to your tailwind.config.js instead:

tsx
module.exports = {
  // ...
  plugins: [
    require('tailwindcss-react-aria-components'),
    require('tailwindcss-animate')
  ]
};

Note: When using Tailwind v3, install tailwindcss-react-aria-components version 1.x instead of 2.x.

</details>

Components

<section className={styles.cardGroup} data-size="small">

<ExampleCard url="../TextField.html" title="TextField" description="A text field allows a user to enter a plain text value with a keyboard."> <TextField /> </ExampleCard>

<ExampleCard url="../Menu.html" title="Menu" description="A menu displays a list of actions or options that a user can choose.">

<Menu style={{background: 'var(--anatomy-gray-100)', width: 'calc(100% - 20px)', padding: 10, maxHeight: 132}} /> </ExampleCard> </section>