Components

Search Bar

A search input with icon, clear button, loading state, and a global Cmd/Ctrl shortcut that focuses it.

input

Installation

npx cano-ui add search-bar

Installs input, kbd, lucide-react automatically if missing.

Source

"use client"

import * as React from "react"
import { Loader2, Search, X } from "lucide-react"

import { cn } from "@/lib/utils"
import { Input } from "@/components/ui/input"
import { KbdGroup } from "@/components/ui/kbd"

export interface SearchBarProps
  extends Omit<React.ComponentProps<"input">, "value" | "onChange" | "size"> {
  value?: string
  defaultValue?: string
  onValueChange?: (value: string) => void
  /** Shows a spinner in place of the clear button. */
  loading?: boolean
  /**
   * Shortcut hint shown while empty, e.g. ["⌘", "K"]. The last key is also
   * registered globally with Cmd/Ctrl to focus the input.
   */
  shortcut?: string[]
  containerClassName?: string
}

export function SearchBar({
  value,
  defaultValue = "",
  onValueChange,
  loading = false,
  shortcut,
  placeholder = "Search…",
  containerClassName,
  className,
  ...props
}: SearchBarProps) {
  const inputRef = React.useRef<HTMLInputElement>(null)
  const [internal, setInternal] = React.useState(defaultValue)
  const current = value ?? internal

  function setValue(next: string) {
    setInternal(next)
    onValueChange?.(next)
  }

  React.useEffect(() => {
    if (!shortcut?.length) return
    const key = shortcut[shortcut.length - 1].toLowerCase()
    function onKeyDown(e: KeyboardEvent) {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === key) {
        e.preventDefault()
        inputRef.current?.focus()
      }
    }
    window.addEventListener("keydown", onKeyDown)
    return () => window.removeEventListener("keydown", onKeyDown)
  }, [shortcut])

  return (
    <div
      data-slot="search-bar"
      className={cn("relative w-full", containerClassName)}
    >
      <Search
        aria-hidden="true"
        className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground"
      />
      <Input
        ref={inputRef}
        type="search"
        role="searchbox"
        value={current}
        onChange={(e) => setValue(e.target.value)}
        placeholder={placeholder}
        className={cn(
          "pl-9 pr-9 [&::-webkit-search-cancel-button]:appearance-none",
          className
        )}
        {...props}
      />
      <span className="absolute top-1/2 right-2 flex -translate-y-1/2 items-center">
        {loading ? (
          <Loader2
            aria-label="Searching"
            className="size-4 animate-spin text-muted-foreground"
          />
        ) : current ? (
          <button
            type="button"
            aria-label="Clear search"
            onClick={() => {
              setValue("")
              inputRef.current?.focus()
            }}
            className="rounded-sm p-0.5 text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
          >
            <X className="size-4" aria-hidden="true" />
          </button>
        ) : shortcut?.length ? (
          <KbdGroup keys={shortcut} aria-hidden="true" />
        ) : null}
      </span>
    </div>
  )
}

This exact file lands in your project at components/ui/search-bar.tsx. It is yours to edit.