Skip to content

Search result row

The row pattern for a search results page. Title + breadcrumb + snippet + matched-term highlighting.

'use client';
import type { ReactNode } from 'react';
import { Stack, Text } from '@helix-ui/core';
export interface SearchResult {
id: string;
title: string;
href: string;
breadcrumb: string[]; // ['Docs', 'Components', 'Button']
snippet: string; // raw text — highlightTerms wraps matches
matchedTerms?: string[];
}
function highlight(text: string, terms: string[] | undefined): ReactNode {
if (!terms || !terms.length) return text;
const re = new RegExp('(' + terms.map(escape).join('|') + ')', 'gi');
return text.split(re).map((part, i) =>
re.test(part) ? (
<mark
key={i}
style={{ background: 'var(--helix-ui-color-bg-action-brand-subtle)', borderRadius: 2 }}
>
{part}
</mark>
) : (
part
),
);
}
function escape(s: string) {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
export function SearchResultRow({ result }: { result: SearchResult }) {
return (
<a
href={result.href}
style={{
display: 'block',
padding: 'var(--helix-ui-space-3) var(--helix-ui-space-4)',
borderRadius: 'var(--helix-ui-radius-md)',
textDecoration: 'none',
color: 'inherit',
}}
className="search-result-row"
>
<Stack gap={1}>
<Text size="xs" tone="muted">
{result.breadcrumb.join(' / ')}
</Text>
<Text weight="semibold" tone="primary">
{highlight(result.title, result.matchedTerms)}
</Text>
<Text size="sm" tone="secondary">
{highlight(result.snippet, result.matchedTerms)}
</Text>
</Stack>
</a>
);
}

Hover state

Add this once to your global stylesheet:

.search-result-row:hover {
background: var(--helix-ui-color-bg-surface-subtle);
}

Cross-cutting hover styles like this belong in CSS, not inline — they don’t compose well via style props.