Components · Data
Search results
An announced count, then ranked results with the address and the match marked in each snippet.
29% of the 2,062 sites in The UI Index have a list (604 sites).
3 results for “keys”
- caseroom › components › tabsTabs
Panels behind a row of tabs. Arrow keys move between tabs, Home and End jump to the ends.
- caseroom › components › kbdKeyboard shortcuts
Show a key or a shortcut, such as ⌘K for the command menu, next to the action it runs.
- caseroom › components › toolbarToolbar
One Tab stop for a row of controls; the arrow keys move between them.
3 results for “keys”
- caseroom › components › tabsTabs
Panels behind a row of tabs. Arrow keys move between tabs, Home and End jump to the ends.
- caseroom › components › kbdKeyboard shortcuts
Show a key or a shortcut, such as ⌘K for the command menu, next to the action it runs.
- 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/ caseroom/ r/ result-list.json - Astro
- npx shadcn@
latest add https:// www.themes.dev/ caseroom/ r/ astro/ result-list.json
Or copy the files below into your project. They need clsx, tailwind-merge, and the Caseroom theme. The HTML needs only the compiled CSS.
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>
);
}/** 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() }));
}---
import { cn } from '@/lib/utils';
import { markParts, resultList as c, type Result } from './result-list.variants';
interface Props {
query: string;
results: Result[];
class?: string;
}
const { query, results, class: className } = Astro.props;
---
<section aria-label="Search results" class={cn(c.root, className)}>
<p role="status" class={c.count}>{results.length} results for “{query}”</p>
<ol class={c.list}>
{
results.map((r) => (
<li class={c.item}>
<span class={c.path}>{r.path}</span>
<a href={r.href} class={c.title}>{r.title}</a>
<p class={c.snippet}>{markParts(r.snippet, query).map((p) => (p.mark ? <mark>{p.text}</mark> : p.text))}</p>
</li>
))
}
</ol>
</section>/** 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() }));
}<section aria-label="Search results" class="w-full max-w-2xl">
<p role="status" class="text-sm text-muted-foreground">3 results for “keys”</p>
<ol class="m-0 mt-4 grid list-none gap-6 p-0">
<li class="grid gap-1">
<span class="truncate font-mono text-xs text-muted-foreground">caseroom › components › tabs</span>
<a href="#tabs" class="-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">
Tabs
</a>
<p class="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">
Panels behind a row of tabs. Arrow
<mark>keys</mark>
move between tabs, Home and End jump to the ends.
</p>
</li>
<li class="grid gap-1">
<span class="truncate font-mono text-xs text-muted-foreground">caseroom › components › kbd</span>
<a href="#kbd" class="-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">
Keyboard shortcuts
</a>
<p class="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">
Show a key or a shortcut, such as ⌘K for the command menu, next to the action it runs.
</p>
</li>
<li class="grid gap-1">
<span class="truncate font-mono text-xs text-muted-foreground">caseroom › components › toolbar</span>
<a href="#toolbar" class="-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">
Toolbar
</a>
<p class="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">
One Tab stop for a row of controls; the arrow
<mark>keys</mark>
move between them.
</p>
</li>
</ol>
</section>