Skip to content
    Style: Wireframe

    Components · Forms

    Search field

    A search input in a role=search form, with a / shortcut and a clear button.

    9.6% of the 2,073 sites in The UI Index have a search (200 sites).

    Both previews render the same markup; the HTML tab below is that markup without a framework.

    Install

    React
    npx shadcn@latest add https://www.themes.dev/ui/r/search-field.json
    Astro
    npx shadcn@latest add https://www.themes.dev/ui/r/astro/search-field.json

    Or copy the files below into your project. They need clsx, tailwind-merge, and the themes.dev UI theme. The HTML needs only the compiled CSS and the script.

    search-field.client.ts
    // Search field behaviour, shared by the Astro, React and HTML versions: "/" (outside any field) focuses the page's
    // first visible [data-search] input; the clear button empties the field, fires input, and puts focus back in it.
    declare global {
      interface Window {
        __caseroomSearch?: boolean;
      }
    }
    
    if (typeof window !== 'undefined' && !window.__caseroomSearch) {
      window.__caseroomSearch = true;
      document.addEventListener('keydown', (e) => {
        if (e.key !== '/' || e.metaKey || e.ctrlKey || e.altKey) return;
        const t = e.target as HTMLElement;
        if (t.closest('input, textarea, select, [contenteditable=""], [contenteditable=true]')) return;
        // The first one you can see (a page may keep others in a closed panel or menu).
        const input = [...document.querySelectorAll<HTMLInputElement>('input[data-search]')].find((i) => i.getClientRects().length > 0);
        if (!input) return;
        e.preventDefault();
        input.focus();
      });
      document.addEventListener('click', (e) => {
        const clear = (e.target as Element).closest<HTMLButtonElement>('[data-search-clear]');
        if (!clear) return;
        const input = clear.parentElement?.querySelector<HTMLInputElement>('input[data-search]');
        if (!input) return;
        input.value = '';
        input.dispatchEvent(new Event('input', { bubbles: true }));
        input.focus();
      });
    }
    
    export {};
    search-field.tsx
    import { cn } from '@/lib/utils';
    import './search-field.client';
    import { CLEAR_PATH, search, SEARCH_PATH } from './search-field.variants';
    
    export interface SearchFieldProps {
      id: string;
      /** The accessible name, e.g. "Search the docs". */
      label: string;
      placeholder?: string;
      /** Where the form goes (GET), e.g. "/search". */
      action?: string;
      name?: string;
      defaultValue?: string;
      className?: string;
    }
    
    /** A search field in a role=search form, with a "/" shortcut and a clear button. */
    export function SearchField({ id, label, placeholder = 'Search', action, name = 'q', defaultValue, className }: SearchFieldProps) {
      return (
        <form role="search" action={action} className={cn(search.root, className)}>
          <label htmlFor={id} className="sr-only">{label}</label>
          <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden="true" className={search.icon}><path d={SEARCH_PATH} /></svg>
          <input type="search" id={id} name={name} defaultValue={defaultValue} placeholder={placeholder} autoComplete="off" data-search="" className={search.input} />
          <kbd className={search.hint} aria-hidden="true">/</kbd>
          <button type="button" data-search-clear="" aria-label="Clear search" className={search.clear}>
            <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden="true"><path d={CLEAR_PATH} /></svg>
          </button>
        </form>
      );
    }
    search-field.variants.ts
    import { fieldClasses } from '../input/input.variants';
    
    /** A search field in a role=search form: magnifier, a native search input, a "/" shortcut hint that gives way while
     *  you type, and a clear button that appears once there is text. "/" anywhere outside a field focuses it. */
    export const search = {
      root: 'relative w-full',
      icon: 'pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground',
      input: `${fieldClasses} peer h-9 py-1 pr-10 pl-9 [&::-webkit-search-cancel-button]:appearance-none`,
      hint: 'pointer-events-none absolute top-1/2 right-2 inline-flex h-5 min-w-5 -translate-y-1/2 items-center justify-center rounded border bg-muted px-1 font-sans text-[11px] text-muted-foreground peer-focus:hidden peer-[:not(:placeholder-shown)]:hidden',
      clear:
        'absolute top-1/2 right-1.5 hidden size-7 -translate-y-1/2 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground active:bg-pressed peer-[:not(:placeholder-shown)]:inline-flex [&_svg]:size-3.5',
    };
    export const SEARCH_PATH = 'M7 12.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM11 11l3.5 3.5';
    export const CLEAR_PATH = 'M4 4l8 8M12 4l-8 8';