Add a VSC Command Palette to Your Website!
Tags: nextjs, react, css, javascript
Published: 2022-01-23 04:38:49

# What We'll Be Making Today

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5mypz69jp0mbuedusxx2.gif) 

[Live Demo 🚀](https://4zsur.sse.codesandbox.io/)

# Wait, what?

[Visual Studio Code](https://code.visualstudio.com/) has an amazing feature called the command palette, which gives you access to the entire application's functionality. It's a productivity hack and saves us a lot of tension.

Similar to [VSC](https://code.visualstudio.com/), [MonkeyType](https://monkeytype.com) is an open-source, minimalistic, and customisable typing test. It's designed to be accessed from just your keyboard, eliminating the need to switch back and forth with your mouse.

I really love the command palette feature on their website too, which allows you to tweak settings easily. Today, we're going to be making something similar to that in Next.js/React.

Looks cool right? Let's get into the tutorial.

# 🚀 Getting Started 

The boring part of setting up a [Next.js project](https://nextjs.org/docs/getting-started) which you probably know how to do.

Feel free to skip to [Creating Our Command Palette](#building-the-palette)

Using [npx](https://www.npmjs.com/package/npx) or [yarn](https://yarnpkg.com/), create a new [Next.js] or [React] project

```sh
# Next.js (flags for TypeScript are optional)

npx create-next-app@latest palette --ts
# ...using yarn
yarn create next-app palette --typescript
```

```sh
# Create React App (flags for TypeScript are optional)

npx create-react-app palette --template typescript
# ...using yarn
yarn create react-app palette --template typescript
```

Since I'm in love with it, I'll be doing this project in [Next.js] with [TypeScript] - however, the procedure for implementing this in other [React] frameworks shouldn't be too different. 

Now let's go to our terminal and run `yarn dev`. Our development server should be started on [localhost:3000](http://localhost:3000). Nice!

# 💡 Project Setup

Alright, now in our `styles/globals.css` file let's add some basic styles to make our page slightly less ugly.

```css
html,
body,
button,
input {
  padding: 0;
  margin: 0;
  color: #e9edf1;
  background-color: #323b45;
  font-family: Fira Code, monospace;
}

a {
  color: inherit;
  text-decoration: none;
}

* {
  box-sizing: border-box;
}
```

Now let's go to `styles/Home.module.css` and create some starter styles for our application.

```css
.container {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: calc(100vh - 2rem);
}
```

Awesome! Let's go to `pages/index.tsx` and remove some junk there. Let's also import our custom monospace font using [next-head](https://nextjs.org/docs/api-reference/next/head).

```jsx
const Home: NextPage = () => {
  return (
    <div>
      <Head>
        <title>Command Palette</title>
        <link
          href='https://fonts.googleapis.com/css2?family=Fira+Code&display=swap'
          rel='stylesheet'
        />
      </Head>
      <div className={styles.container}>
        <h1>Press Space For a Command Palette</h1>
      </div>
    </div>
  )
}
```

Our page should now look somewhat like this:

![image](https://user-images.githubusercontent.com/69592270/150664950-a78c6034-8144-4903-8605-15c72aadd5ae.png)


# 🎨 Building the Palette

Cool! So let's start building out our command palette.

Create a new file, `components/Palette.tsx` and open it up.

Inside it, let's define the basic structure of our command palette. 

```jsx
const Palette: React.FC = () => {
  return (
    <div role='dialog' aria-modal='true'>
      <div>
        <div>
          { /* Search Icon */ }
          <input
            type='text'
            placeholder='Type to Search...'
            spellCheck='false'
          />
        </div>
        <div>
          <a
            href='https://github.com/harshhhdev'
            rel='norefferer'
           >
             { /* GitHub Icon */ }
             GitHub
            </a>
        </div>
      </div>
    </div>
  )
}
```

Save it, and import this component into the main file.

If you check our browser now, the grotesque default styles make me want to vomit. Let's go back to our `styles/Home.module.css` file and add in some extra styles.

```css
.container {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
}

.palette {
  position: fixed;
  min-height: 100vh;
  width: 100vw;
  background: rgba(0, 0, 0, 0.5);
  user-select: none;
  z-index: 1;
}

.wrapper {
  background-color: #323b45;
  max-height: 50vh;
  height: fit-content;
  width: 35rem;
  margin: 8% auto;
  left: 0;
  right: 0;
  border-radius: 20px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  z-index: 999;
}

.options {
  width: 35rem;
  overflow: hidden scroll;
}

.options::-webkit-scrollbar {
  width: 5px;
}

.options::-webkit-scrollbar-thumb {
  background-color: #939faf;
}

.search {
  display: flex;
  align-items: center;
  width: 35rem;
  height: 4rem;
  padding: 1.25rem;
  color: #939faf;
}

.search > svg {
  margin-right: 1.25rem;
}

.input {
  border: none;
  outline: none;
  background: none;
  color: #939faf;
  font-size: 1.25rem;
}

.option {
  display: flex;
  align-items: center;
  width: 35rem;
  height: 2.5rem;
  padding: 0.625rem 1.25rem;
  color: #939faf;
  background-color: #323b45;
  font-size: 0.875rem;
  border: none;
  text-decoration: none;
}

.option:hover,
.option:focus {
  color: #323b45;
  background-color: #939faf;
  cursor: pointer;
}

.option > svg {
  margin-right: 0.625rem;
}
```

Awesome! We can now add these styles to our components accordingly. 

While we're at it, let's also download some icons from [React Icons](https://react-icons.github.io/react-icons/), and import them up top.

```jsx
const Palette: React.FC = () => {
  return (
    <div className={styles.palette} role='dialog' aria-modal='true'>
      <div className={styles.wrapper}>
        <div className={styles.contents}>
          <div className={styles.search}>
            <FaSearch />
            <input
             type='text'
             placeholder='Type to Search...'
             spellCheck='false'
             className={styles.input}
            />
          </div>
          <div className={styles.options}>
            <a
              href='https://github.com/harshhhdev'
              rel='norefferer'
              className={styles.option}
             >
               <FaGithub />
               GitHub
              </a>
          </div>
        </div>
      </div>
    </div>
  )
}
```

Phew, that sure looks better. Now let's create multiple links using an array and map it onto the command line.

```ts
const Options = [
  {
    href: 'https://hxrsh.in/',
    title: 'Portfolio',
    icon: FaUser,
  },
  {
    href: 'https://github.com/harshhhdev',
    title: 'GitHub',
    icon: FaGithub,
  },
  {
    href: 'https://twitter.com/harshhhdev/',
    title: 'Twitter',
    icon: FaTwitter,
  },
]
```

Under `div.options`, let's map the array onto an option element

```jsx
{Options.map((option, index) => (
  <a 
    className={styles.option}
    href={option.href}
    rel='norefferer'
    key={index}
   >
     <option.icon />
     {option.title}
    </a>
))}
```

...and with that, woo-hoo! Our command line's visual aspect is finished 🎉 It should look something like this

![image](https://user-images.githubusercontent.com/69592270/150659528-0b5acc35-925a-4f3f-8d9f-9d58a2e89fd9.png)

# 🔎 Searching

Let's get the search working. To start off, let's create a filter function which filters our array using a search query.

```ts
const filter = (options: typeof Options, query: string) => {
  if (!query) return options

  return options.filter((option) => {
    const optionText = option.title.toLowerCase()
    return optionText.includes(query.toLowerCase())
  })
}
```

Awesome! Now let's get our search bar input and filter the results using some React hooks.

```ts
const inputRef = useRef<HTMLInputElement>(null)
const [searchQuery, setSearchQuery] = useState('')
```

Let's create another function, called changeHandler, which will be called when we type something onto the search bar.

```ts
const changeHandler = (e: React.FormEvent<HTMLInputElement>) => {
  e.preventDefault()
  setSearchQuery(e.currentTarget.value)
}
```

Then, let's create a new array of objects being passed through the filter function which returns an array of objects with keywords.

```ts
const filteredResults = filter(Options, searchQuery)
```

Now, instead of mapping `Options`, let's map `filteredResults`

```jsx
{filteredResults.map((option, index) => (
  <a 
    className={styles.option}
    href={option.href}
    rel='norefferer'
    key={index}
   >
     <option.icon />
     {option.title}
    </a>
))}
```

The search should be working perfectly now! Give yourself a pat on the back if you made it this far.

# 💻 Keyboard Navigation

One of the crucial characteristics of our command palette should be the ability to navigate it using only the keyboard.

For this, we'll be using a handy React hook called [react-arrow-key-navigation-hook](https://github.com/dealroom/react-arrow-key-navigation-hook). 

If you're using [TypeScript], unfortunately, this project does not come with type definitions. Either make your own type definitions, turn off library checks in `tsconfig.json`, or just create a file called `nav.d.ts` paste in `declare module 'react-arrow-key-navigation-hook'`

After we've done that, we can import it into our project:

```jsx
import useArrowKeyNavigationHook from 'react-arrow-key-navigation-hook'

// ...inside the Palette component
const parentRef = useArrowKeyNavigationHook({ selectors: 'a,input' })
```

Let's assign the ref into our `wrapper`

```jsx
<div className={styles.wrapper} ref={parentRef}>
```

Awesome! We should now be able to navigate through our command palette using just our arrow keys.

# Toggle States

As we prepare to wrap up, we need to add in toggling functionality to our palette. 

Let's create another state:

```js
const [open, setOpen] = useState(false)
```

When we close, we also need to reset our `searchQuery`. To do this, let's create a toggle function.

```ts
const toggle = () => {
  setSearchQuery('')
  setOpen((open) => !open)
}
```

Now, let's add an event listener to our document inside the `useEfffect`

```js
  useEffect(
    () =>
      document.addEventListener(
        'keydown',
        (e) => e.key === ' ' && toggle()
      ),
    []
  )
```

Here, we add a document listener for the "space" key being pressed. When it's pressed, we simply toggle the state of the command palette.

We should now be able to toggle the state of our palette with the Space key. Nice! However, we also want it so that when the click outside the palette, it should close. 

You can create your own hook for this following [this tutorial](https://www.30secondsofcode.org/react/s/use-click-outside), however, for the sake of this tutorial I'll just use [react-detect-click-outside](https://github.com/zhaluza/react-detect-click-outside).

```ts
const boundary = useDetectClickOutside({ onTriggered: toggle })
```

Now let's assign our `boundary` to our `contents` element

```tsx
<div className={styles.contents} ref={boundary}>
```

...and viola! The div should work.

# Wrapping Up

That's it! There are libraries which do this for you, but nothing's as good as building your own from scratch as it leaves you limitless for customising it the way you want!

Now for those looking for some extra fun or learning something extra, here are some additions you could add to this:

 - Animations using [Framer Motion](https://framer.com/motion)
 - Create sub-menus using React hooks
 - Add a radio button functionality

And that's it! Feel free to checkout the live demo on [CodeSandbox](https://codesandbox.io/s/command-palette-react-4zsur)

{% codesandbox 4zsur %}

[Next.js]: https://nextjs.org
[React]: https://reactjs.org
[TypeScript]: https://typescriptlang.org