Find
A find-in-view card with a field, a match count and stepping controls, and an optional list of results beneath them.
Installation
import { Find } from "@o/pipeline/experimental/components/find";
Default
Type in the field to search the messages below it. ⏎ and ⇧⏎ step through the matches, and ↓ opens the list of messages that contain one.
- MCMaya Chen · 9:12Northwind want the pricing sheet before Thursday's call.
- OOperate · 9:14I drafted a pricing summary from the last three calls with Northwind.
- SOSam Ortiz · 9:31Looks good. Can we move the security review to Friday?
- MCMaya Chen · 9:40Friday works. I'll send Priya the updated pricing tiers tonight.
- SOSam Ortiz · 10:05 · in Northwind procurementTheir procurement team asked whether pricing is per seat or per workspace.
- OOperate · 10:06 · in Northwind procurementPer seat, with a workspace minimum of ten seats.
- MCMaya Chen · 11:20 · in Security reviewSecurity questionnaire is back, nothing blocking.
Usage
Find is a compound set of parts built on Base UI Autocomplete, rendered inline and always open, so the results list sits inside the card rather than in a popup of its own. It doesn’t search anything itself: you own the query, the matches, which match is current, and whether the list is shown.
- Query: controlled through
Root’svalueandonValueChange. Choosing a row never rewrites the query. - Stepping:
Root’sonStep(direction)receives"next"or"previous"from the keyboard and fromPreviousandNext. Move your current match and scroll it into view. - Results: pass
itemsas groups ({ value, items }), already filtered to the rows that match. Drop a group with no rows, or its label renders on its own. - List:
listOpenandonListOpenChangedecide whether the list is shown. The field asks to open it on ↓ or ↑, whenitemshas a row to show, and to close it on Esc. - Closing:
onClosefires on Esc with the list closed and fromClose. Unmount the card in response. - Warm on intent:
onItemHighlightedfires as a row is highlighted by keyboard or pointer, to preview its match before it’s chosen. - Row value:
itemToStringValuesets what a row reads as. By default each row reads as the query.
Annotate the List render param with your item type, since like Base UI’s Autocomplete it isn’t inferred from Root’s items. Group and Item infer theirs from their props.
<Find.Root
items={groups}
value={query}
onValueChange={setQuery}
listOpen={listOpen}
onListOpenChange={setListOpen}
onStep={step}
onClose={close}
>
<Find.Bar>
<Find.Input />
<Find.Count current={current} total={matches.length} />
<Find.Previous />
<Find.Next />
<Find.Close />
</Find.Bar>
<Find.List>
{(group: FindResultGroup<Message>) => (
<Find.Group key={group.value} group={group}>
{(message) => (
<Find.Item
key={message.id}
value={message}
symbol={<MessageAvatar message={message} />}
description={<Snippet message={message} query={query} />}
meta={message.time}
onClick={() => jumpTo(message)}
>
{message.author}
</Find.Item>
)}
</Find.Group>
)}
</Find.List>
</Find.Root>
Anatomy
Root: the card. It takes the popup surface (bg-elevated,rounded-lg,shadow-popup) and Pipeline popups’ entry through@starting-style, since it isn’t a Base UI popup. It has no exit animation, so unmount it when it closes.Bar: the 40px row the field, the count and the controls share.Input: the field. Itsplaceholder(default “Find in page”) is also its accessible name.Count: the match count. SeeCount.Previous,Next: circular ghostIconButtons that callonStep, each with a tooltip naming its shortcut.Close: the same kind of button callingonClose, with a divider before it.List: the results beneath the bar, rendered whilelistOpenis set anditemshas a row (orstatushas something to say). Itschildrenrender each group, and itsstatusprop holds a status row, such as a loading one, after the groups.Group: a group’s label and its rows, whichchildrenrenders.hideLabelkeeps the label for screen readers only, for a list whose rows explain themselves.Item: one result on two lines, built onInternalChoice: a leadingsymbol(an avatar or an icon, shown at 20px) beside the title (children) withmetasuch as a time at the end of its line, and a one-linedescriptionbeneath. The highlighted row shows ⏎ at the end of the description’s line, under the meta, so the title line never shifts.Mark: a match inside a row’s text, tinted withfind-match.
Count
Count shows current/total (“2/6”) and announces “2 of 6”. Leave current unset before a match is chosen, and it shows the total alone. With total at 0 it shows empty, “No results” by default, so render it only once there’s a query. Pass children to replace the count with a status the numbers can’t say.
List
Open the list with ↓ from the field. Each row shows a snippet with the query marked, and choosing a row is yours to handle through its onClick; it fires on pointer click and on ⏎ while the row is highlighted.
- MCMaya Chen · 9:12Northwind want the pricing sheet before Thursday's call.
- OOperate · 9:14I drafted a pricing summary from the last three calls with Northwind.
- SOSam Ortiz · 9:31Looks good. Can we move the security review to Friday?
- MCMaya Chen · 9:40Friday works. I'll send Priya the updated pricing tiers tonight.
- SOSam Ortiz · 10:05 · in Northwind procurementTheir procurement team asked whether pricing is per seat or per workspace.
- OOperate · 10:06 · in Northwind procurementPer seat, with a workspace minimum of ten seats.
- MCMaya Chen · 11:20 · in Security reviewSecurity questionnaire is back, nothing blocking.
The list’s height is capped at eight two-line rows (425px), enough for every filter hint without scrolling, and it never runs past the bottom of the visual viewport, so a phone’s software keyboard doesn’t cover its last rows. To change the cap, set ---find-list-max-height through List’s className or style, which land on the list’s scroll box, where the default is declared:
<Find.List className="[---find-list-max-height:--spacing(80)]">
No results
With no matching rows, pass items={[]}: the list stays hidden even while listOpen is set, and comes back by itself when the query finds rows again. Count shows “No results”, and the stepping buttons can be disabled.
- MCMaya Chen · 9:12Northwind want the pricing sheet before Thursday's call.
- OOperate · 9:14I drafted a pricing summary from the last three calls with Northwind.
- SOSam Ortiz · 9:31Looks good. Can we move the security review to Friday?
- MCMaya Chen · 9:40Friday works. I'll send Priya the updated pricing tiers tonight.
- SOSam Ortiz · 10:05 · in Northwind procurementTheir procurement team asked whether pricing is per seat or per workspace.
- OOperate · 10:06 · in Northwind procurementPer seat, with a workspace minimum of ten seats.
- MCMaya Chen · 11:20 · in Security reviewSecurity questionnaire is back, nothing blocking.
Keyboard
In the field:
- ⏎ / ⇧⏎: call
onStepwith"next"/"previous". While the list is open with a row highlighted, ⏎ chooses that row instead. - ⌘G / ⇧⌘G on Apple platforms, Ctrl+G / Ctrl+⇧G elsewhere: call
onStep, like the browser’s own “find again”, whether or not a row is highlighted. - ↓ / ↑: with the list closed and a row to show, ask to open it; with no rows they do nothing. With it open, Base UI moves the highlight through the rows.
- Esc: with the list open, ask to close it; otherwise call
onClose. The field prevents the key’s default and stops it propagating.
Accessibility
Countis anAutocomplete.Status, so changes to it are announced. It shows “2/6” but reads “2 of 6”, and reads “6 results” before a match is chosen.- With
inline, Base UI sets noaria-expandedon the field, andaria-controlsnames the list only while it’s shown. Previous,NextandClosetake their accessible names from theirlabelprops, which default to “Previous match”, “Next match” and “Close”.
Theme tokens
Find.Mark tints matches in a row with find-match. Hosts that mark matches in the view being searched, such as a transcript, use the same tokens:
find-match: behind every match, a light green tint (16% in light, 22% in dark).find-match-currentandfind-match-current-foreground: the matchFindis on, solid and inverted so it stands out among the others (green-700 under white text in light, a light green under near-black text in dark).
With the CSS Custom Highlight API they read as --color-find-match, --color-find-match-current and --color-find-match-current-foreground:
::highlight(find-match) {
background-color: var(--color-find-match);
}
::highlight(find-match-current) {
background-color: var(--color-find-match-current);
color: var(--color-find-match-current-foreground);
}
::highlight() can’t round corners, so marks in a transcript are square, while Find.Mark is rounded-xs.