Churn rose in August mostly among teams that never finished setup. Accounts that completed the checklist kept renewing at the usual rate.
Suggestions
Follow-up questions after an answer, where it's always clear whether a tap fills the composer or sends right away.
Fill: edit before asking
Send: ask immediately
Installation
npx shadcn@latest add @pixeldosa/suggestionsThe example above also uses Message. To copy it as-is, install everything it needs:
npx shadcn@latest add @pixeldosa/suggestions @pixeldosa/messageRequires `cn` at `@/lib/utils`. Default to `mode="fill"`. Use `send` only when the suggestions are exactly what the user would type. Pass `streaming` from the message status.
Usage
"use client";
import { Suggestions } from "@/components/ui/suggestions";
<Suggestions
items={["Which teams didn't finish setup?", "Draft an onboarding email"]}
streaming={message.status === "streaming"}
onSelect={(prompt) => composer.fill(prompt)}
/>
Why it exists
Suggestion chips usually hide the one thing that matters: does tapping this send a message? Sending words someone didn't write can't be undone in a conversation. Here the mode is a choice you make, and it's visible.
Features
- Fill or send, visibly. A pencil means the text goes into the composer to edit. An arrow means it's asked immediately. Screen readers hear "Edit before asking:" or "Ask:".
- Safe by default.
mode="fill"unless you choose otherwise. - Not mid-stream. Hidden while the answer is still arriving.
- Chips or list. Wrapping pills for short prompts, full-width rows for longer questions.
- Label and prompt can differ. Show "Compare with July" and ask the full question.
- One tab stop. Arrow keys, Home and End move between suggestions.
Examples
Send mode, for prompts that are exactly what the user would type:
<Suggestions mode="send" layout="list" items={followUps} onSelect={(prompt) => send(prompt)} />
A short label that asks a fuller question:
<Suggestions
items={[{ id: "july", label: "Compare with July", prompt: "Compare August churn with July, by plan." }]}
onSelect={fill}
/>
Props
| Prop | Type | Description |
|---|---|---|
items | (string | { id, label, prompt? })[] | Required. |
onSelect | (prompt, item) => void | Required. |
mode | "fill" | "send" | Defaults to "fill". |
streaming | boolean | Hides the suggestions. |
layout | "chips" | "list" | Defaults to "chips". |
label | string | Group name. Defaults to "Suggested follow-ups". |
Accessibility
- A named group with a single tab stop and arrow-key movement.
- Each item's name says what a tap does.
- List rows are 44px tall, and chips extend their hit area vertically.
When not to use
This isn't autocomplete. For completing text as someone types, use Ghost Input. For suggestions
inside the composer before the first message, use Prompt Composer's suggestions.
why it's built this way
Engineering Notes
Most suggestion chips hide their most important property: whether tapping one sends a message. Sending words the user didn't write can't be taken back in a conversation, so the component makes mode explicit in three places: the trailing glyph (pencil for fill, arrow for send), the accessible name ('Edit before asking:' vs 'Ask:'), and the default, which is the safer `fill`. Items accept a separate `prompt`, so a short visible label can expand into a fuller question. Suggestions render nothing while `streaming`, because a follow-up to an unfinished answer is premature and often wrong once the answer completes. Keyboard behaviour follows the toolbar pattern already used in AI Action Toolbar: one tab stop with roving tabindex, arrows in both axes (so it works for wrapping chips and stacked lists), and Home and End. The active index resets if the list shrinks. Chips extend their hit area vertically with a transparent pseudo-element, and list rows are 44px tall.
how it moves, and when it doesn't
Motion Notes
One entrance, then stillness, because choosing a follow-up is the user's turn. Items fade in with the shared `pd-fade-in` at `--pd-duration-base` on the decelerate curve, staggered 40ms per item so the row reads left to right rather than popping in as a block. There's no slide or scale, so text is readable from the first frame. Hover colour runs at `--pd-duration-instant`. Nothing shimmers or pulses. Under prefers-reduced-motion the entrance is removed and items appear immediately.