Smart Pick
betaA follow-up question that opens where a change landed, with the app's best guesses first and a search for anyone else.
Preview
search
Who should review this?
guesses
bump
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/smart-pick
Use it for
- Ask the one thing a change left open, right where it landed, like who should review a ticket that was just filed.
- Rank suggestions with what the app already knows, best first, and say why in reason, like "Reviewed the last two".
- The 1, 2, and 3 keys pick the first three guesses from anywhere on the page. Each guess's tooltip teaches its key. The keys are never printed.
- Pass options, the full list, to add a search field for anyone else.
- Show it, and what replaces it once picked, inside <PanelSwitch effect="fade">, so the height glides instead of jumping.
- onDismiss runs on Esc and on the close button. Leave the answer empty then, and let people fill it in later.
- DeltaBump, from the same file, shows a change like "+5" rising from the value it changed. Give it a new key to play it again.
Avoid it for
- A question that must be answered before going on. Use a Dialog.
- More than three or four suggestions. Past three, they have no key, and the guess is not a guess.
- Opening it on page load. It follows a change the person just made.
- Two open at once. Only the newest hears the number keys.
Props
- title
The question, like "Who should review this?". It names the group and the list.
string
- hint
A quiet line under the question, like "Ticket 482 by Ava Patel".
string
- suggestions
Required. The best guesses, best first. Each is { id, label, reason?, leading?, keywords? }. The first three answer to 1, 2, and 3.
SmartPickOption[]
- options
The full list. With it, a search field finds anyone, matching label and keywords.
SmartPickOption[]
- onPick
Runs with the option picked by a press, Enter, or 1 to 3.
(option: SmartPickOption) => void
- onDismiss
Runs on Esc, with no search typed, and on the close button. Leaves the question unanswered.
() => void
- placeholder
The search field's placeholder. Its name drops the ellipsis.
string · default "Someone else…"
- SmartPickOption.leading
Before the name, like an avatar or initials.
ReactNode
- DeltaBump
Part. "+5" rising from the value it changed, then gone. Put it in a relative box around the value.
{ value: string; tone?: "neutral" | "brand" | "danger" | "success" | "warning" }
- tone
On DeltaBump. The change's color.
"neutral" | "brand" | "danger" | "success" | "warning" · default "neutral"
- className
Extra classes on the box. Later classes win, so className='p-2' replaces the padding.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| title | string | - | The question, like "Who should review this?". It names the group and the list. |
| hint | string | - | A quiet line under the question, like "Ticket 482 by Ava Patel". |
| suggestions | SmartPickOption[] | - | Required. The best guesses, best first. Each is { id, label, reason?, leading?, keywords? }. The first three answer to 1, 2, and 3. |
| options | SmartPickOption[] | - | The full list. With it, a search field finds anyone, matching label and keywords. |
| onPick | (option: SmartPickOption) => void | - | Runs with the option picked by a press, Enter, or 1 to 3. |
| onDismiss | () => void | - | Runs on Esc, with no search typed, and on the close button. Leaves the question unanswered. |
| placeholder | string | "Someone else…" | The search field's placeholder. Its name drops the ellipsis. |
| SmartPickOption.leading | ReactNode | - | Before the name, like an avatar or initials. |
| DeltaBump | { value: string; tone?: "neutral" | "brand" | "danger" | "success" | "warning" } | - | Part. "+5" rising from the value it changed, then gone. Put it in a relative box around the value. |
| tone | "neutral" | "brand" | "danger" | "success" | "warning" | "neutral" | On DeltaBump. The change's color. |
| className | string | - | Extra classes on the box. Later classes win, so className='p-2' replaces the padding. |
Accessibility
- It is a group named by its title. The guesses are a listbox of options, and each keyed guess carries aria-keyshortcuts.
- It opens with focus left where it was. 1 to 3 and Esc reach it from anywhere on the page, but not from a field that types text, or a menu or dialog opened above it, or while it is inert.
- With a search field, the field is a combobox: ↑ and ↓ move the highlight, Enter picks, and aria-activedescendant points at the highlight. Without one, the list is the Tab stop and does the same, with Home and End too.
- The pointer and the keys share one highlight. A finger does not highlight.
- Esc clears a typed search first, then leaves the question unanswered. It stops there, so a Sheet around it stays open.
- "No one matches" is said through a status message.
- DeltaBump is hidden from screen readers. Say the change where the value is.
- Option text is accent-foreground on the accent fill when highlighted, a pair the theme tests hold to 4.5:1 in light and dark.
Examples
Pick, then show the answer in place (needs @txbrendan/panel-switch)
"use client";
import { useState } from "react";
import { PanelSwitch } from "@/components/ui/panel-switch";
function Reviewer({ people }: { people: SmartPickOption[] }) {
const [reviewer, setReviewer] = useState<SmartPickOption | null>(null);
const [open, setOpen] = useState(true);
return (
<PanelSwitch effect="fade" value={open ? "pick" : "done"}>
{open ? (
<SmartPick
title="Who should review this?"
hint="Ticket 482 by Ava Patel"
suggestions={people.slice(0, 3)}
options={people}
onPick={(option) => {
setReviewer(option);
setOpen(false);
}}
onDismiss={() => setOpen(false)}
/>
) : (
<p>Reviewer: {reviewer?.label ?? "Not set"}</p>
)}
</PanelSwitch>
);
}Guesses only, with no search
<SmartPick
title="Who owns this incident?"
suggestions={[
{ id: "tom", label: "Tom Hale", reason: "On call this week" },
{ id: "jo", label: "Jo Okafor" },
]}
onPick={(option) => save(option.id)}
/>A value that bumps by +2
<span className="relative">
19
<DeltaBump key={bumps} value="+2" tone="success" />
</span>