Thursday, June 30, 2022
HomeWeb DevelopmentReact command palette with Tailwind CSS and Headless UI

React command palette with Tailwind CSS and Headless UI


As builders, we frequently try to optimize our workflows as a lot as attainable, saving time by leveraging instruments just like the terminal. A command palette is one such software that shows latest exercise in an online or desktop utility, enabling fast navigation, quick access to instructions, and shortcuts, amongst different issues.

To raise your productiveness stage, a command palette is basically a UI element that takes the type of a modal. A command palette is particularly helpful in massive, advanced purposes with many transferring components, for instance, the place it’d take you many clicks or skimming by a number of dropdowns to entry a useful resource.

On this tutorial, we’ll discover find out how to construct a completely useful command palette from scratch utilizing the Headless UI Combobox element and Tailwind CSS.

Actual-world use instances for a command palette

As a developer, there’s a really excessive probability that you simply’ve used a command palette earlier than. The most well-liked one is the VS Code command palette, however there are lots of different examples, together with the GitHub Command Palette, Linear, Figma, Slack, monkeytype, and extra.

The GitHub app

GitHub not too long ago launched a command palette function that’s nonetheless in public beta on the time of writing. It helps you to shortly soar to totally different pages, seek for instructions, and get options based mostly in your present context. You too can slim the scope of the assets you’re searching for by tabbing into one of many choices or utilizing a particular character:

Github Command Palette

The Linear app

Should you’re not acquainted with Linear, it’s a undertaking administration software just like Jira and Asana that gives a extremely nice consumer expertise. Linear has a really intuitive command palette that permits you to entry your entire utility’s performance with its keyboard-first design. On this tutorial, we’ll construct a command palette just like Linear:

Linear App Command Palette

Important options of a command palette

A number of trendy purposes are implementing command palettes as a function, however what makes a great command palette element? Right here’s a concise checklist of issues to look out for:

  • A easy shortcut to open the palette, i.e., ctrl + ok
  • It may be accessible from wherever within the utility
  • It has in depth search options, comparable to fuzzy search
  • Instructions talk intent and are simple to grasp
  • It offers entry to each a part of the appliance from one place

Within the subsequent part, we’ll construct our personal element that features all of the options listed above. Let’s get into it!

Constructing the element

The command palette will not be really as advanced because it appears, and anybody can construct one shortly. I’ve ready a starter undertaking for this tutorial so to simply comply with alongside. The starter undertaking is a React and Vite SPA that replicates the Linear points web page.

Organising the undertaking

To get began, clone the repository into your native listing, set up the mandatory dependencies, and begin the event server. The undertaking makes use of Yarn, however when you’re extra snug with npm or pnPm, you may delete the yarn.lock file earlier than operating npm set up or pnpm set up:

// clone repository
$ git clone https://github.com/Mayowa-Ojo/command-palette
// swap to the 'starter-project' department
$ git checkout starter-project
// set up dependencies
$ yarn
// begin dev server
$ yarn dev

Should you go to localhost:3000, you’ll see the next web page:

Github Repository Clone

The CommandPalette element

Subsequent, we’ll construct the element. We’ll use the Headless UI combobox and dialog parts. combobox would be the base element for our command palette. It has built-in options like focus administration and keyboard interplay. We’ll use the dialog element to render our command palette in a modal.

To model the parts, we’ll use Tailwind CSS. Tailwind is a CSS utility library that permits you to simply add inline kinds in your HTML or JSX recordsdata. The starter undertaking already consists of the configuration for Tailwind.

Set up the mandatory dependencies as follows:

$ yarn add @headlessui/react @heroicons/react

Within the parts folder, create a CommandPalette.jsx file and add the next code block:

import { Dialog, Combobox } from "@headlessui/react";

export const CommandPalette = ({ instructions }) => {
  const [isOpen, setIsOpen] = useState(true);

  return (
    <Dialog
      open={isOpen}
      onClose={setIsOpen}
      className="mounted inset-0 p-4 pt-[15vh] overflow-y-auto"
    >
      <Dialog.Overlay className="mounted inset-0 backdrop-blur-[1px]" />
      <Combobox
         as="div"
         className="bg-accent-dark max-w-2xl mx-auto rounded-lg shadow-2xl relative flex flex-col"
         onChange={(command) => {
            // we now have entry to the chosen command
            // a redirect can occur right here or any motion might be executed
            setIsOpen(false);
         }}
      >
         <div className="mx-4 mt-4 px-2 h-[25px] text-xs text-slate-100 bg-primary/30 rounded self-start flex items-center flex-shrink-0">
            Difficulty
         </div>
         <div className="flex items-center text-lg font-medium border-b border-slate-500">
            <Combobox.Enter
               className="p-5 text-white placeholder-gray-200 w-full bg-transparent border-0 outline-none"
               placeholder="Sort a command or search..."
            />
         </div>
         <Combobox.Choices
            className="max-h-72 overflow-y-auto flex flex-col"
            static
         ></Combobox.Choices>
      </Combobox>
   </Dialog>
  );
};

Just a few issues are occurring right here. First, we import the Dialog and Combobox parts. Dialog is rendered as a wrapper across the Combobox, and we initialize an area state known as isOpen to manage the modal.

We render a Dialog.Overlay contained in the Dialog element to function the overlay for the modal. You may model this nonetheless you need, however right here, we’re simply utilizing backdrop-blur. Then, we render the Combobox element and cross in a handler operate to the onChange prop. This handler known as each time an merchandise is chosen within the Combobox. You’d usually wish to navigate to a web page or execute an motion right here, however for now, we simply shut the Dialog.

Combobox.Enter will deal with the search performance, which we’ll add later on this part. Combobox.Choices renders a ul aspect that wraps the checklist of outcomes we’ll render. We cross in a static prop that signifies we wish to ignore the internally managed state of the element.

Subsequent, we render our CommandPalette within the App.jsx file:

const App = () => {
   return (
      <div className="flex w-full bg-primary h-screen max-h-screen min-h-screen overflow-hidden">
         <Drawer groups={groups} />
         <AllIssues points={points} />
         <CommandPalette instructions={instructions}/>
      </div>
   );
};

Let’s discuss how our command palette will operate. We’ve a listing of predefined instructions within the information/seed.json file. These instructions can be displayed within the palette when it’s opened and might be filtered based mostly on the search question. Pretty easy, proper?

The CommandGroup element

CommandPalette receives a instructions prop, which is the checklist of instructions we imported from seed.json. Now, create a CommandGroup.jsx file within the parts folder and add the next code:

// CommandGroup.jsx
import React from "react";
import clsx from "clsx";
import { Combobox } from "@headlessui/react";
import { PlusIcon, ArrowSmRightIcon } from "@heroicons/react/strong";
import {
   CogIcon,
   UserCircleIcon,
   FastForwardIcon,
} from "@heroicons/react/define";
import { ProjectIcon } from "../icons/ProjectIcon";
import { ViewsIcon } from "../icons/ViewsIcon";
import { TemplatesIcon } from "../icons/TemplatesIcon";
import { TeamIcon } from "../icons/TeamIcon";

export const CommandGroup = ({ instructions, group }) => {
   return (
      <React.Fragment>
         {/* solely present the header when there are instructions belonging to this group */}
         {instructions.filter((command) => command.group === group).size >= 1 && (
            <div className="flex items-center h-6 flex-shrink-0 bg-accent/50">
               <span className="text-xs text-slate-100 px-3.5">{group}</span>
            </div>
         )}
         {instructions
            .filter((command) => command.group === group)
            .map((command, idx) => (
               <Combobox.Possibility key={idx} worth={command}>
                  {({ lively }) => (
                     <div
                        className={clsx(
                           "w-full h-[46px] text-white flex items-center hover:bg-primary/40 cursor-default transition-colors duration-100 ease-in",
                           lively ? "bg-primary/40" : ""
                        )}
                     >
                        <div className="px-3.5 flex items-center w-full">
                           <div className="mr-3 flex items-center justify-center w-4">
                              {mapCommandGroupToIcon(
                                 command.group.toLowerCase()
                              )}
                           </div>
                           <span className="text-sm text-left flex flex-auto">
                              {command.title}
                           </span>
                           <span className="text-[10px]">{command.shortcut}</span>
                        </div>
                     </div>
                  )}
               </Combobox.Possibility>
            ))}
      </React.Fragment>
   );
};

We’re merely utilizing the CommandGroup element to keep away from some repetitive code. Should you take a look at the Linear command palette, you’ll see that the instructions are grouped based mostly on context. To implement this, we have to filter out the instructions that belong to the identical group and repeat that logic for every group.

The CommandGroup element receives two props, instructions and group. We’ll filter the instructions based mostly on the present group and render them utilizing the Combobox.Possibility element. Utilizing render props, we will get the lively merchandise and magnificence it accordingly, permitting us to render the CommandGroup for every group within the CommandPalette whereas conserving the code clear.

Be aware that we now have a mapCommandGroupToIcon operate someplace within the code block above. It is because every group has a special icon, and the operate is only a helper to render the right icon for the present group. Now, add the operate just under the CommandGroup element in the identical file:

const mapCommandGroupToIcon = (group) => {
   swap (group) {
      case "situation":
         return <PlusIcon className="w-4 h-4 text-white"/>;
      case "undertaking":

Now, we have to render the CommandGroup element in CommandPalette.
Import the element as follows:

import { CommandGroup } from "./CommandGroup";

Render it contained in the Combobox.Choices for every group:

<Combobox.Choices
   className="max-h-72 overflow-y-auto flex flex-col"
   static
>
   <CommandGroup instructions={instructions} group="Difficulty"/>
   <CommandGroup instructions={instructions} group="Venture"/>
   <CommandGroup instructions={instructions} group="Views"/>
   <CommandGroup instructions={instructions} group="Staff"/>
   <CommandGroup instructions={instructions} group="Templates"/>
   <CommandGroup instructions={instructions} group="Navigation"/>
   <CommandGroup instructions={instructions} group="Settings"/>
   <CommandGroup instructions={instructions} group="Account"/>
</Combobox.Choices>

You need to see the checklist of instructions being rendered now. The following step is to wire up the search performance.

Implementing the search performance

Create an area state variable in CommandPalette.jsx:

// CommandPalette.jsx
const [query, setQuery] = useState("");

Go the state replace handler to the onChange prop in Combobox.Enter. The question can be up to date with each character you kind within the enter field:

<Combobox.Enter
  className="p-5 text-white placeholder-gray-200 w-full bg-transparent border-0 outline-none"
  placeholder="Sort a command or search..."
  onChange={(e) => setQuery(e.goal.worth)}
/>

One of many key properties of a great command palette is in depth search performance. We will simply do a easy string comparability of the search question with the instructions, nonetheless that wouldn’t account for typos and context. A a lot better resolution that doesn’t introduce an excessive amount of complexity is a fuzzy search.

We’ll use the Fuse.js library for this. Fuse.js is a strong, light-weight, fuzzy search library with zero dependencies. Should you’re not acquainted with fuzzy looking, it’s a string matching method that favors approximate matching over the precise match, implying that you could get right options even when the question has typos or misspellings.

First, set up the Fuse.js library:

$ yarn add fuse.js

In CommandPalette.jsx, instantiate the Fuse class with a listing of instructions:

// CommandPalette.jsx
const fuse = new Fuse(instructions, { includeScore: true, keys: ["name"] });

The Fuse class accepts an array of instructions and configuration choices. The keys discipline is the place we register what fields are within the instructions checklist to be listed by Fuse.js. Now, create a operate that can deal with the search and return the filtered outcomes:

// CommandPalette.jsx
const filteredCommands =
  question === ""
     ? instructions
     : fuse.search(question).map((res) => ({ ...res.merchandise }));

We test if the question is empty, return all of the instructions, and if not, run the fuse.search methodology with the question. Additionally, we’re mapping the outcomes to create a brand new object. That is to take care of consistency as a result of the outcomes returned by Fuse.js have some new fields and won’t match the construction we have already got.

Now, cross the filteredCommands to the instructions prop in every CommandGroup element. It ought to appear to be the code beneath:

// CommandPalette.jsx
<CommandGroup instructions={filteredCommands} group="Difficulty"/>
<CommandGroup instructions={filteredCommands} group="Venture"/>
<CommandGroup instructions={filteredCommands} group="Views"/>
<CommandGroup instructions={filteredCommands} group="Staff"/>
<CommandGroup instructions={filteredCommands} group="Templates"/>
<CommandGroup instructions={filteredCommands} group="Navigation"/>
<CommandGroup instructions={filteredCommands} group="Settings"/>
<CommandGroup instructions={filteredCommands} group="Account"/>

Attempt looking within the command palette and see if the outcomes are being filtered:

Command Palette Search Filter

We’ve a completely useful command palette, however you would possibly discover that it’s at all times open. We want to have the ability to management its open state. Let’s outline a keyboard occasion that can pay attention for a key mixture and replace the open state. Add the next code to CommandPalette.jsx:

// CommandPalette.jsx
useEffect(() => {
  const onKeydown = (e) => {
     if (e.key === "ok" && (e.metaKey || e.ctrlKey)) {
        e.preventDefault();
        setIsOpen(true);
     }
  };
  window.addEventListener("keydown", onKeydown);
  return () => {
     window.removeEventListener("keydown", onKeydown);
  };
}, []);

We’re utilizing a useEffect Hook to register a keydown keyboard occasion when the element is mounted, and we use a clean-up operate to take away the listener when the element unmounts.

Within the Hook, we test if the important thing mixture matches ctrl + ok. If it does, then the open state is ready to true. You too can use a special key mixture, but it surely’s necessary to not use combos that conflict with the native browser shortcuts.

That’s it! You will discover the completed model of this undertaking on the finished-project department.

react-command-palette: Prebuilt element

We’ve explored find out how to construct a command palette element from scratch. Nonetheless, you’d in all probability reasonably not construct your individual each time you want a command palette. That’s the place a prebuilt element might be helpful. Most element libraries don’t supply a command palette, however react-command-palette is a effectively written element that’s accessible and browser appropriate.

To make use of this element, set up it as a dependency in your undertaking:

$ yarn add react-command-palette

Import the element and cross your checklist of instructions to it as follows:

import React from "react";
import CommandPalette from 'react-command-palette';

const instructions = [{
  name: "Foo",
  command() {}
},{
  name: "Bar",
  command() {}
}]

export default operate App() {
  return (
    <div>
      <CommandPalette instructions={instructions} />
    </div>
  );
}

There are plenty of config choices that you should utilize to customise the look and habits to fulfill your necessities. For instance, the theme config helps you to select from various built-in themes or create your individual customized theme.

Subsequent steps

On this article, you’ve realized about command palettes, the best use instances for them, and what options make up a great command palette. You’ve additionally explored in detailed steps find out how to construct one utilizing the Headless UI combobox element and Tailwind CSS.

Should you simply wish to shortly ship this function in your utility, then a prebuilt element like react-command-palette is the way in which to go. Thanks for studying, and remember to depart a remark when you have any questions.

Is your frontend hogging your customers’ CPU?

As net frontends get more and more advanced, resource-greedy options demand an increasing number of from the browser. Should you’re thinking about monitoring and monitoring client-side CPU utilization, reminiscence utilization, and extra for all your customers in manufacturing, attempt LogRocket.https://logrocket.com/signup/

LogRocket is sort of a DVR for net and cellular apps, recording all the pieces that occurs in your net app or website. As a substitute of guessing why issues occur, you may combination and report on key frontend efficiency metrics, replay consumer classes together with utility state, log community requests, and robotically floor all errors.

Modernize the way you debug net and cellular apps — .

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments