AI
Suggested Prompts
Click-to-send conversation starters and follow-ups — a wrapping strip of one-shot cards that latches after the first pick, goes inert while the assistant streams, and re-rolls without collapsing the row.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/suggested-prompts.jsonPrompt
Build a React + TypeScript + Tailwind "SuggestedPrompts" component with zod and
lucide-react, reusing a roving-tabindex hook.
Contract
- A zod schema (`suggestedPromptSchema` in a sibling contract file) is the single
source of truth for one card: { id, text, icon? } where icon is a closed enum
of glyph keys ("sparkles" | "search" | "code" | "file" | "chart" | "idea") —
a contract carries data, never a ReactNode, and a closed set fails parsing
instead of rendering a hole. `suggestedPromptsStatusSchema` is
"loading" | "empty" | "error" | "ready". One refine on the array rejects
duplicate ids (the send lock keys on id); guard `Array.isArray` inside it,
because a refine still runs on a payload whose inner shape failed and a bare
dereference would throw out of safeParse instead of returning an error.
- There is deliberately NO separate short "label" field. The card renders the
exact string that will be sent, because the click is irreversible — a chip
that says "Summarize" while sending three sentences is a trap. The cost of
that honesty is a batch that mixes 8-character and 200-character cards, which
the layout has to absorb.
- Props: items; status; onSelect(item) [required]; streaming?; onRefresh?;
refreshing?; resetKey?; label? (default "Suggested", "" hides the eyebrow);
refreshLabel?; skeletonCount? (clamped 1–8); className. Affordances are
handler-gated: no onRefresh means no re-roll button anywhere, including the
error branch.
Behavior
- MISFIRE IS THE WHOLE PROBLEM. A click sends for real and cannot be taken back,
so two guards run before onSelect: (1) `busy` — streaming, refreshing, or a
"loading" status still showing the previous batch — and (2) a one-shot latch.
The latch is a ref written synchronously inside the handler, not state: React
does not re-render between two clicks dispatched in the same task, so a
state-only guard lets a double tap through. Measured: three synchronous
`.click()` calls on one card produce 3 sends with a state guard and 1 with the
ref guard.
- Once a card is spent the WHOLE group renders aria-disabled — never the native
`disabled` attribute, which blurs the element the instant it flips and drops
the reader's focus to `body`, and which would also make "a click still reaches
the handler and is refused" untestable. Do not add pointer-events:none for the
same reason: the click must arrive and be turned down.
- The latch clears when the batch changes. "Changed" is decided by the ids' own
content (plus an optional `resetKey`), not array identity — a consumer that
builds `items` inline would otherwise re-arm the group on every keystroke
elsewhere in the page. Adjust the state during render, not in an effect, so
there is never a frame showing fresh suggestions in a locked group.
- Re-rolling must not make the row jump. `refreshing` keeps the current cards
mounted and inert while the next batch loads, and a `loading` status arriving
with items still present also keeps them (items outrank a transient status).
The only branch that renders zero height is the empty one — which returns null
outright: no wrapper, no heading, no leftover gap under the message.
- Keyboard: the strip is ONE tab stop. Left/Right (plus Home/End) move a roving
tabindex through the cards in DOM order, exactly one card carries tabIndex 0,
and Enter/Space on a focused card sends it. Up/Down are deliberately left
alone so the page can still scroll; make the group vertical only if you stack
the cards.
Rendering & styling
- These are optional commands, not form options: a `div` with role="group",
aria-labelledby pointing at the visible eyebrow (aria-label when it is
hidden), aria-describedby pointing at an sr-only line that says choosing one
sends it immediately, and plain `button` children. No radio/checkbox roles and
no aria-checked/aria-pressed — those would tell a screen-reader user they must
pick one, and that a pick is reversible.
- Layout wraps (flex-wrap + items-start), it is not a horizontal rail: an 8- and
a 200-character suggestion cannot share a fixed-width track without either
clipping the long one or padding the short one, and a rail also hides its last
card from keyboard users. Cards take their natural width, cap at max-w-full,
and long ones simply own a row.
- The text span carries `wrap-anywhere`, not `break-words`: only
`overflow-wrap: anywhere` lowers min-content width. Measured at 375px with a
111-character unbreakable URL — with it the page scrollWidth is 365 against a
375 client width; with `break-words` instead, 500.
- Semantic tokens only: bg-card + border for a card, hover:bg-accent and
hover:border-primary/50, border-primary + bg-primary/5 + a Check glyph for the
card that was just sent, bg-muted for skeletons, text-muted-foreground for the
eyebrow and the error line, text-destructive for the error glyph, and
focus-visible:ring-2 ring-ring everywhere. cn() merges the consumer className
into the root.
- Only the colour transitions, the skeleton pulse and the re-roll spinner
animate, each with a motion-reduce:*-none twin; with motion off every one of
them still works.
Customization levers
- Send semantics: keep onSelect one-shot. If your product wants "fill the
composer instead of sending", drop the latch and the streaming block — that is
a different component, and it should look different too (no latch, no dimming).
- Density: px-3 py-2 text-sm reads as a card; h-8 px-3 rounded-full with the
icon removed turns the same strip into pill-shaped quick replies.
- Batch policy: `resetKey` re-arms without changing `items` (same suggestions
offered on a new turn); returning a genuinely new batch re-arms by itself.
- Chrome: `label=""` drops the eyebrow, omitting onRefresh drops the re-roll
everywhere, `skeletonCount` should match the number of suggestions you
usually return, and the icon enum is the natural place to add your own glyphs.
- Focus hand-off after a send is the consumer's call: keep the group mounted
through the reply (focus stays on the spent card) or move focus to the
transcript yourself — unmounting the strip on click drops focus to `body`.Concepts
- Click-to-send — the card is a command, not a choice. It carries no selected state, nothing toggles back off, and the accessible name is the exact text that will leave, because the reader gets no confirmation step.
- One-shot latch — the first accepted click spends the entire batch, not just the card that was pressed. The guard lives in a ref updated inside the handler, so two clicks landing in the same task (double tap, synthetic pair) still produce exactly one send; a state-only guard measured 3.
- Inert, not disabled — while the assistant streams, cards keep their native
disabled-free markup and are markedaria-disabled. They stay focusable, they still receive the click, and the handler turns it down — a card that removes itself from the tab order mid-reply would strand keyboard focus onbody. - Stale-batch hold — a re-roll keeps the current cards on screen until the next batch lands, so the strip's height moves from one real value to another instead of collapsing to zero and shoving the transcript. The only zero-height branch is the empty one, and that one renders no node at all.
- Wrap over rail — suggestions are 8 to 200 characters. A wrapping flow lets a long one own a row and a short one stay small; a horizontal rail would need a fixed track (clipping the long) and would push its last card out of keyboard reach.
- Content-keyed re-arm — "is this a new batch" is answered from the ids' content plus an optional
resetKey, never from array identity, so a consumer that rebuildsitemson every render cannot silently unlock a spent group.
Citation Chip
The inline [n] marker that trails a sentence in an AI answer — an inline box that cannot change the paragraph's line height, with a source card portalled past every clipping bubble.
Artifact Panel
A side panel for the file the assistant keeps rewriting — a revision rail with the prompt behind each one, preview / source / changes, per-revision scroll memory, and copy plus download that always take the revision you are looking at.