Nextjs + Kbar
How to build a command K interface with Nextjs and Kbar?
Enable the search functionality with the command + K interface.
--
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
- next-themes
- @tailwindcss/typography
- react-hot-toast
- 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
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
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: id is always unique for your actions.
- Name: Enter the Name of your action
- Shortcut: Enter the Shortcut name; if someone types the shortcut key. Then it runs an associate task based on action.
- Keywords: Pass additional information about the action
- Section: Divide your action into deference sections.
- 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.
- Icon: Show icon-related action for visual effect.
- 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
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" />,
},
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 ResultItem
component 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.