Simple Search

The clean ⌘K search this site uses, with a rotating placeholder, results that blur in and a filter menu.

Preview

Setup

Install with the shadcn CLI

$npx shadcn@latest add https://kinetikui.vercel.app/r/simple-search.json

One command adds every file, the npm packages and any Kinetik components it builds on. Needs a components.json; the CLI offers to create one if your project has none.

Or install manually

  1. 1

    Run in your terminal:

    $npm install clsx tailwind-merge framer-motion lucide-react tw-animate-css
  2. 2

    Copy lib/utils.ts from the Code tab. Skip if you already have cn().

  3. 3

    Copy styles/tokens.css from the Code tab and import it in your global CSS after Tailwind: @import "./styles/tokens.css";

  4. 4

    Create a components/ui/simple-search/ folder and copy every file from the Code tab into it. Each file does one job, so you can read or change a piece without the rest.

  5. 5

    Import and render:

    Example
    import { SimpleSearch } from "@/components/ui/simple-search"
    
    <SimpleSearch
      items={[{ title: "Button", href: "/docs/button", group: "Components" }]}
      onSelect={(item) => router.push(item.href!)}
    />

Usage

This is the same search as the one in this site's navbar. Press ⌘K / Ctrl K to try it on real pages.

Pass it a flat list of items. Each item belongs to a group, which is used for the result headings and the filter menu. While the field is empty, the placeholder cycles through your item titles.

import { useRouter } from "next/navigation"
import { SimpleSearch, type SimpleSearchItem } from "@/components/ui/simple-search"

const items: SimpleSearchItem[] = [
  { title: "Home", href: "/", group: "Pages" },
  { title: "Button", href: "/docs/button", group: "Components", badge: "New" },
  { title: "Theming", href: "/docs/theming", group: "Guides", description: "Colours and dark mode" },
]

export function Search() {
  const router = useRouter()
  return <SimpleSearch items={items} onSelect={(item) => item.href && router.push(item.href)} />
}

To open it from your own button, control it with open and onOpenChange and set showTrigger={false}.

Props

PropTypeDefaultNotes
itemsSimpleSearchItem[]—Matched by title, group, description and keywords.
onSelect(item) => void—Runs on Enter or click, after the dialog closes.
hotkeystring | false"k"Letter that toggles the dialog with ⌘ / Ctrl.
openboolean—Controlled open state. Pair with onOpenChange.
onOpenChange(open) => void—Runs whenever the dialog opens or closes.
showTriggerbooleantrueRender the built-in search button.
showFilterbooleantrueShow the group filter in the footer.
onFilterChange(group, direction) => void—Hook for analytics or sounds when the filter changes.
labelstring"Search"Accessible name for the dialog and input.
inputPropsInputHTMLAttributes—Extra props for the input, e.g. data attributes.
SimpleSearchItemTypeNotes
titlestringShown and searched.
groupstringResult heading and filter option.
hrefstring?Not used by the component; handy in onSelect.
descriptionstring?Second line, also searched.
badgestring?Small pill after the title, e.g. New.
keywordsstring?Extra hidden words to match.

Accessibility

  • The input is a combobox that controls a listbox; the highlighted result is exposed with aria-activedescendant
  • ↑ ↓ move through results across groups, Enter opens, Escape closes and returns focus to the trigger
  • The filter menu is its own listbox with arrow keys; Escape closes just the menu
  • Animations are skipped when the visitor prefers reduced motion