How to build a command K interface with Nextjs and Kbar?
command CTRL + K

Nextjs + Kbar

How to build a command K interface with Nextjs and Kbar?

Enable the search functionality with the command + K interface.

Rajdeep singh
FrontEnd web
Published in
11 min readApr 27

--

Now a day, the web is faster and more feature-rich. One of my favorite features is the command + k interface. It increases user interaction and engagement out of the box.

The command + k interface creates a theme toggle, search bar, navigation, etc., on your site. Without writing complex code, you achieve a similar function to copy-paste code.

All the code is available on GitHub. You can also check out the live demo website.

Let's start

· Create a new project.
· Install the Kbar package
· Additional node packages
@tailwindcss/typography
next-themes
react-icons
react-hot-toast
· Configuration
· Actions
Schema
· Search Box
· Dynamic Schema
· Nested Schema
· Render Result
· Conclusion

Create a new project.

The first step is to create a new nextjs project using create-next-app cli. In this project, we use tailwind css and typescript. I select both packages on nextjs installation time.

npx create-next-app@latest
# or
yarn create next-app
# or
pnpm create next-app

Install the Kbar package.

To create a command + k interface, you need a kbar nodejs package. You can install it with the following command.

npm install kbar
# or
yarn add kbar
# or
pnpm add kbar

Kbar is a compulsory package. You can't create the command + k interface without the kbar package; there is no alternative.

Additional node packages

It would be best if you had some additional packages

  1. next-themes
  2. @tailwindcss/typography
  3. react-hot-toast
  4. react-icons

@tailwindcss/typography

@tailwindcss/typography package for handling dynamic typography with Tailwind CSS.

next-themes

The next-themes package enables themes like switching from dark to light mode on your site.

react-icons

The react-icons package provides lots of SVG icons for the project. This way, you do not need to download them manually.

react-hot-toast

Add beautiful notifications to your React app with react-hot-toast.

Configuration

The first step is to warp intra-site with the KBarProvider component. After you are eligible to use the Kbar Search and result component inside nextjs.

// src/pages/_app.tsx

import { KBarProvider } from "kbar";

export default function App({ Component, pageProps }: AppProps) {

return (
<KBarProvider>
<Component {...pageProps} />
</KBarProvider>
)
}

If you use a next-themes package with Kbar, create a separate Layout component in your project and put your KBarProvider inside Layout component.

// src/components/Layout.tsx

import { useRouter } from "next/router";
import { KBarProvider } from "kbar";

export default function Layout({ children }:{children: React.ReactNode}) {

return (<KBarProvider>
{children}

{/* ... rest of code */}

</KBarProvider>)
}

Then wrap the entire site with the Layout component.


import '@/styles/globals.css'
import type { AppProps } from 'next/app'
import { ThemeProvider } from 'next-themes'
import Layout from "@/components/Layout"
import { Toaster } from "react-hot-toast";

export default function App({ Component, pageProps }: AppProps) {

/* This code is the main App component in a Next.js application. It is responsible for
rendering the layout of the application and providing a theme to the entire app using the
`ThemeProvider` component from the `next-themes` library. */

return (
<ThemeProvider attribute="class">

<Layout>

<Component {...pageProps} />

</Layout>

{/* toaster component */}
<Toaster
toastOptions={{
position: "bottom-right",
}}
/>

</ThemeProvider>
)
}

The toaster component help to show notification on the site. When somebody toggles the theme on the site, it shows a notification.

Actions

Actions

For example, Home, Docs, Contact, Twitter, etc, are actions in Kbar. The Kdar package provides fix schema structure for action.

In the simple word, You can define any task. In Kbar, it is called a schema.

Schema

Schema definition
Schema definition
const actions:Action[] = [
{
id: "homeAction",
name: "Home",
shortcut: ["h"],
keywords: "back",
section: "Navigation",
perform: () => router.push("/"),
icon: <FaHome className="w-6 h-6 mx-3" />,
subtitle: "Subtitles can help add more context.",

},

// ...

]
  1. Id: id is always unique for your actions.
  2. Name: Enter the Name of your action
  3. Shortcut: Enter the Shortcut name; if someone types the shortcut key. Then it runs an associate task based on action.
  4. Keywords: Pass additional information about the action
  5. Section: Divide your action into deference sections.
  6. Perform: Run task if users click on the action; you can redirect the user, show an alert message, change the theme from dark to light, etc.
  7. Icon: Show icon-related action for visual effect.
  8. Subtitle: Enter subtitle information related to the action.
// Layout.tsx

import { useRouter } from "next/router";
import toast from "react-hot-toast";
import { KBarAnimator, KBarPortal, KBarPositioner, KBarSearch, KBarProvider, ActionImpl,Action } from "kbar";
import { useTheme } from 'next-themes';
import RenderResults from "@/components/RenderResults";
import { FaHome, FaGithub, FaPhoneAlt, FaTwitter, FaBook, FaRegSun, FaSun, FaMoon, FaSearch } from "react-icons/fa";
import React from "react"

export default function Layout({ children }:{children: React.ReactNode}) {

// Toggle theme
const { setTheme } = useTheme()

// redirect the user

const router = useRouter();

// define the Action of KBar
const actions:Action[] = [
{
id: "homeAction",
name: "Home",
shortcut: ["h"],
keywords: "back",
section: "Navigation",
perform: () => router.push("/"),
icon: <FaHome className="w-6 h-6 mx-3" />,
subtitle: "Subtitles can help add more context.",
},
{
id: "docsAction",
name: "Docs",
shortcut: ["g", "d"],
keywords: "help",
section: "Navigation",
icon: <FaBook className="w-6 h-6 mx-3" />,
perform: () => router.push("/docs"),
},
{
id: "contactAction",
name: "Contact",
shortcut: ["c"],
keywords: "email hello",
section: "Navigation",
icon: <FaPhoneAlt className="w-6 h-6 mx-3" />,
perform: () => window.open("timchang@hey.com", "_blank"),
},
{
id: "twitterAction",
name: "Twitter",
shortcut: ["g", "t"],
keywords: "social contact dm",
section: "Navigation",
icon: <FaTwitter className="w-6 h-6 mx-3" />,
perform: () => window.open("https://twitter.com/timcchang", "_blank"),
},
{
id:"githubAction",
name: "Github",
shortcut: ["g", "h"],
keywords: "sourcecode",
section: "Navigation",
icon: <FaGithub className="w-6 h-6 mx-3" />,
perform: () => window.open("https://github.com/timc1/kbar", "_blank"),
},
{
id: "blog",
name: "Search Blogs",
shortcut: ["?"],
keywords: "serach articles",
section: "blog",
icon: <FaSearch className="w-6 h-6 mx-3" />
},
{
id: "theme",
name: "Change theme…",
keywords: "interface color dark light",
section: "Preferences",
icon: <FaRegSun className="w-6 h-6 mx-3" />,
},
{
id: "darkTheme",
name: "Dark",
keywords: "dark theme",
section: "Preferences",
perform: () => {

// change the theme
setTheme("dark");

// Show the message on screen, when somebody change theme.

toast.success(`Now dark theme is apply`, {
icon: '👏',
style: {
borderRadius: '10px',
background: '#333',
color: '#fff',
},
})
},
icon: <FaMoon className="w-6 h-6 mx-3" />,
parent: "theme",
},
{
id: "lightTheme",
name: "Light",
keywords: "light theme",
section: "Preferences",
perform: () => {

// change the theme
setTheme("light")

// Show the message on screen, when somebody change theme.

toast.success(`Now light theme is apply`, {
icon: '👏',
style: {
borderRadius: '10px',
background: '#333',
color: '#fff',
},
})
},
icon: <FaSun className="w-6 h-6 mx-3" />,
parent: "theme",
},

];

// pass the action into KBarProvider

return (<KBarProvider actions={actions} options={{ enableHistory: true }}>

{children}

{/* ... rest of code */}

</KBarProvider>
)
}

Search Box

Show Search Box

After defining your action schema for Kbar, the next step shows your search card box on your website.

To show Search Box on Enter site, make sure to add your Search Box on _app.tsx page.

// Layout.tsx

<KBarPortal>
<KBarPositioner>
<KBarAnimator className="max-w-3xlLspInfo w-3/6 bg-white border-r-8 overflow-hidden shadow-white ">
<KBarSearch className="py-4 px-5 text-xs w-full outline-none border-none bg-white text-black " />
<RenderResults />
</KBarAnimator>
</KBarPositioner>
</KBarPortal>

The final result looks like this.

// Layout.tsx


import { useRouter } from "next/router";
// show message
import toast from "react-hot-toast";
import { KBarAnimator, KBarPortal, KBarPositioner, KBarSearch, KBarProvider, ActionImpl,Action } from "kbar";
// toggle theme
import { useTheme } from 'next-themes';
import RenderResults from "@/components/RenderResults";
import { FaHome, FaGithub, FaPhoneAlt, FaTwitter, FaBook, FaRegSun, FaSun, FaMoon, FaSearch } from "react-icons/fa";
import React from "react"


export default function Layout({ children }:{children: React.ReactNode}) {

// toggle theme
const { setTheme } = useTheme()

// router
const router = useRouter();

// actions

const actions:Action[] = [
{
id: "homeAction",
name: "Home",
shortcut: ["h"],
keywords: "back",
section: "Navigation",
perform: () => router.push("/"),
icon: <FaHome className="w-6 h-6 mx-3" />,
subtitle: "Subtitles can help add more context.",
},
{
id: "docsAction",
name: "Docs",
shortcut: ["g", "d"],
keywords: "help",
section: "Navigation",
icon: <FaBook className="w-6 h-6 mx-3" />,
perform: () => router.push("/docs"),
},
{
id: "contactAction",
name: "Contact",
shortcut: ["c"],
keywords: "email hello",
section: "Navigation",
icon: <FaPhoneAlt className="w-6 h-6 mx-3" />,
perform: () => window.open("timchang@hey.com", "_blank"),
},
{
id: "twitterAction",
name: "Twitter",
shortcut: ["g", "t"],
keywords: "social contact dm",
section: "Navigation",
icon: <FaTwitter className="w-6 h-6 mx-3" />,
perform: () => window.open("https://twitter.com/timcchang", "_blank"),
},
{
id:"githubAction",
name: "Github",
shortcut: ["g", "h"],
keywords: "sourcecode",
section: "Navigation",
icon: <FaGithub className="w-6 h-6 mx-3" />,
perform: () => window.open("https://github.com/timc1/kbar", "_blank"),
},
{
id: "blog",
name: "Search Blogs",
shortcut: ["?"],
keywords: "serach articles",
section: "blog",
icon: <FaSearch className="w-6 h-6 mx-3" />
},
{
id: "theme",
name: "Change theme…",
keywords: "interface color dark light",
section: "Preferences",
icon: <FaRegSun className="w-6 h-6 mx-3" />,
},
{
id: "darkTheme",
name: "Dark",
keywords: "dark theme",
section: "Preferences",
perform: () => {
setTheme("dark");
toast.success(`Now dark theme is apply`, {
icon: '👏',
style: {
borderRadius: '10px',
background: '#333',
color: '#fff',
},
})
},
icon: <FaMoon className="w-6 h-6 mx-3" />,
parent: "theme",
},
{
id: "lightTheme",
name: "Light",
keywords: "light theme",
section: "Preferences",
perform: () => {
setTheme("light")
toast.success(`Now light theme is apply`, {
icon: '👏',
style: {
borderRadius: '10px',
background: '#333',
color: '#fff',
},
})
},
icon: <FaSun className="w-6 h-6 mx-3" />,
parent: "theme",
},

];

return (<KBarProvider options={{ enableHistory: true }} actions={actions}>

{children}

<KBarPortal>
<KBarPositioner>
<KBarAnimator className="max-w-3xlLspInfo w-3/6 bg-white border-r-8 overflow-hidden shadow-white ">
<KBarSearch className="py-4 px-5 text-xs w-full outline-none border-none bg-white text-black " />
<RenderResults />
</KBarAnimator>
</KBarPositioner>
</KBarPortal>

</KBarProvider>)
}

Design your search box card with tailwind CSS.

Dynamic Schema

You can pass dynamic data to KBar actions. For example, purpose I added my dynamic article schema with use help of useRegisterAction hook.

// Card.tsx


import Link from 'next/link'
import React from 'react'
import { useRegisterActions } from "kbar";
import { useRouter } from 'next/router';


export const Card = ({ item }: { item: { title: string; description: string; tags: string[]; } }) => {
const router = useRouter();

// Pass dynamic schema to Actions

useRegisterActions([{
id: item.title,
name: item.title,
keywords: item.description,
shortcut: [],
perform: () => router.push("/blog-my-title"),
parent: "blog",
}]);

return (
<div className="p-12 md:w-1/2 flex flex-col items-start">
<span className="inline-block py-1 px-2 rounded bg-indigo-50 text-indigo-500 text-xs font-medium tracking-widest">{item.tags[0]}</span>
<h2 className="sm:text-3xl dark:text-white text-2xl title-font font-medium text-gray-900 mt-4 mb-4">{item.title}</h2>
<p className="leading-relaxed mb-8 dark:text-gray-400">{item.description}</p>
<div className="flex items-center flex-wrap pb-4 mb-4 border-b-2 border-gray-100 mt-auto w-full">
<Link href={"/blog-my-title"} className="text-indigo-500 inline-flex items-center">Learn More </Link >
</div>
</div>
)
}

Nested Schema

Firstly create a new parent schema; in the scheme, make sure your id is unique and add a name, keyword, and section.

{

id: "theme",
name: "Change theme…",
keywords: "interface color dark light",
section: "Preferences",
icon: <FaRegSun className="w-6 h-6 mx-3" />,

},
Child schema ( Task )
Child schema ( Task )

After creating a parent, then we create a child schema. For child schema, you add a parent property in your schema. The parent property uses your parent ID to make the nested schema.

{
id: "darkTheme",
name: "Dark",
keywords: "dark theme",
section: "Preferences",
perform: () => {
setTheme("dark");
toast.success(`Now dark theme is apply`, {
icon: '👏',
style: {
borderRadius: '10px',
background: '#333',
color: '#fff',
},
})
},
icon: <FaMoon className="w-6 h-6 mx-3" />,
parent: "theme",
},

Render Result

Render each action on the screen, and Kbar provides the KBarResult component.

Import KBarResults and useMatches hook from the Kbar package and use it. KBarResults component onRender option render accepts a JSX component.

The useMatches hook matches the search result, and its returns to KBarResults . I create a separate ResultItemcomponent to handle the search UI.

// RenderResults.tsx

import { KBarResults, useMatches } from "kbar";
import ResultItem from "./ResultItem";

export default function RenderResults() {


/* `const { results } = useMatches();` is using the `useMatches` hook from the `kbar` library to get
the search results. It destructures the `results` property from the object returned by the hook,
which contains an array of search results. These results can be used to render the search results in
the UI. */

const { results } = useMatches();

return (
<KBarResults
items={results}
onRender={({ item, active }) => {
return typeof item === "string" ? (
<div className="py-3 px-5"> <h2 className="text-center uppercase"> {item} </h2> </div>
) : (
<ResultItem
action={item}
active={active}
/>
)
}
}
/>
);
}

React.forwardRef() help remove the item's css misbehavior.

// ResultItem.tsx

import * as React from "react";
import { ActionImpl } from "kbar";

// Forward Ref
const ResultItem = React.forwardRef(

function ResultItem({ action, active}: { action: ActionImpl; active: boolean;},ref: React.Ref<HTMLDivElement>) {

return (
<div ref={ref} className={active ? `px-3 py-2 leading-none rounded text-violet11 flex items-center justify-between bg-violet4` : `px-3 py-2 leading-none rounded text-violet11 flex items-center justify-between hover:bg-violet4`} >
<header className="flex items-center">
{action.icon}
<div className="rounded flex flex-col items-start justify-center relative select-none outline-none hover:bg-violet4">
<h1 className="text-lg text-violet11"> {action.name} </h1>
<p className="text-md text-violet9 py-1"> {action.subtitle} </p>
</div>
</header>
<div className="text-[15px] leading-none text-violet11 rounded flex justify-between items-center relative select-none outline-none hover:bg-violet4">
{action.shortcut?.length ? (
<div
aria-hidden
style={{ display: "grid", gridAutoFlow: "column", gap: "4px" }}
>
{action.shortcut.map((sc) => (
<kbd
key={sc}
style={{
padding: "4px 6px",
background: "rgba(0 0 0 / .1)",
borderRadius: "4px",
fontSize: 14,
}}
>
{sc}
</kbd>
))}
</div>
) : null}
</div>
</div>
);
})

export default ResultItem

Conclusion

Enabling the command + k interface with nextjs is an easy process. You can provide great user experiences to your client and visitor. It helps you to increase user engagement and interaction on-site.

I never feel any performance issues with Kbar. Adding KBar is the right choice, and it is not time-wasting.

I recommended every big site, such as documentation and dashboard, provide a command + K interface to increase the user experience.

You can share and follow us on Twitter and Linkedin. If you like my work, please read more content on the officialrajdeepsingh.dev, frontend web, and Sign up for a free newsletter.

You can also check out awesome-next, a curated list of excellent Nextjs-based libraries that help build small and large-scale applications with next.js.

--

--

Rajdeep singh
FrontEnd web

JavaScript || Reactjs || Nextjs || Python || Rust || Biotechnology || Bioinformatic || Front-end Developer || Author https://officialrajdeepsingh.dev/