PixelDosa UI

Suggestions

Follow-up questions after an answer, where it's always clear whether a tap fills the composer or sends right away.

Assistant

Churn rose in August mostly among teams that never finished setup. Accounts that completed the checklist kept renewing at the usual rate.

Fill: edit before asking

Send: ask immediately

Installation

bash
npx shadcn@latest add @pixeldosa/suggestions

The example above also uses Message. To copy it as-is, install everything it needs:

bash
npx shadcn@latest add @pixeldosa/suggestions @pixeldosa/message

Requires `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

PropTypeDescription
items(string | { id, label, prompt? })[]Required.
onSelect(prompt, item) => voidRequired.
mode"fill" | "send"Defaults to "fill".
streamingbooleanHides the suggestions.
layout"chips" | "list"Defaults to "chips".
labelstringGroup 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.