Skip to content
    Style: Wireframe

    Components · Data

    Search results

    An announced count, then ranked results with the address and the match marked in each snippet.

    30% of the 2,073 sites in The UI Index have a list (612 sites).

    3 results for “keys”

    1. caseroom › components › tabsTabs

      Panels behind a row of tabs. Arrow keys move between tabs, Home and End jump to the ends.

    2. caseroom › components › kbdKeyboard shortcuts

      Show a key or a shortcut, such as ⌘K for the command menu, next to the action it runs.

    3. caseroom › components › toolbarToolbar

      One Tab stop for a row of controls; the arrow keys move between them.

    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/result-list.json
    Astro
    npx shadcn@latest add https://www.themes.dev/ui/r/astro/result-list.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.

    result-list.tsx
    import { cn } from '@/lib/utils';
    import { markParts, resultList as c, type Result } from './result-list.variants';
    
    export type { Result };
    
    export interface ResultListProps {
      query: string;
      results: Result[];
      className?: string;
    }
    
    /** Search results: an announced count, then ranked results with the match marked in each snippet. */
    export function ResultList({ query, results, className }: ResultListProps) {
      return (
        <section aria-label="Search results" className={cn(c.root, className)}>
          <p role="status" className={c.count}>{results.length} results for “{query}”</p>
          <ol className={c.list}>
            {results.map((r) => (
              <li key={r.href} className={c.item}>
                <span className={c.path}>{r.path}</span>
                <a href={r.href} className={c.title}>{r.title}</a>
                <p className={c.snippet}>{markParts(r.snippet, query).map((p, k) => (p.mark ? <mark key={k}>{p.text}</mark> : p.text))}</p>
              </li>
            ))}
          </ol>
        </section>
      );
    }
    result-list.variants.ts
    /** Search results: a count line (role=status, so a new search is announced), then results as an ordered list (a
     *  real ranking): the title as the link, the address in small type, a snippet with the match marked (<mark>). The
     *  title link carries the row's target; rows are separated by space, not rules. */
    export const resultList = {
      root: 'w-full',
      count: 'text-sm text-muted-foreground',
      list: 'm-0 mt-4 grid list-none gap-6 p-0',
      item: 'grid gap-1',
      path: 'truncate font-mono text-xs text-muted-foreground',
      title: '-mx-1 w-fit rounded-sm px-1 text-base font-medium underline decoration-transparent underline-offset-4 transition-[text-decoration-color] hover:decoration-foreground active:bg-accent',
      snippet: 'max-w-prose text-sm text-pretty text-muted-foreground [&_mark]:rounded-sm [&_mark]:bg-accent [&_mark]:px-0.5 [&_mark]:font-medium [&_mark]:text-foreground',
    };
    export type Result = { title: string; href: string; path: string; snippet: string };
    
    /** The snippet as parts, with every match of the query marked (case-insensitive). */
    export function markParts(text: string, query: string): { text: string; mark: boolean }[] {
      const q = query.trim();
      if (!q) return [{ text, mark: false }];
      const re = new RegExp(`(${q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
      return text.split(re).filter(Boolean).map((t) => ({ text: t, mark: t.toLowerCase() === q.toLowerCase() }));
    }