KQ-1730 · QUESTION/TRAIL
Question Trail
A conversational intake, one question at a time: each question is typed first, then its bubble rises a step from its tail corner on springs.snap, and the option you pick travels into place as your answer chip on springs.glide. Pressing an earlier chip rewinds — the later bubbles fold back up into it bottom-first in a cascade, heights closing on glide while their words lift away on the exit ease — and answering again replays the trail forward with every kept answer until a question has none, so a changed branch only asks what is new. Options are a roving group of buttons (arrows move, Enter sends, Escape keeps the old answer) and text questions are labelled fields that say what is wrong.
Book a call-out
Basinworks · answers kept as you go
2 answered · leak branch · waiting on how soon
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/question-trail
Props
- questions
- QuestionTrailQuestion[]
- default defaultQuestionTrailQuestions
- { id, prompt, short?, kind?, options?, placeholder?, optional?, next?, validate? } — next is an id, a map from answer to id (a branch), or null to end.
- value / defaultValue
- Record<string, string>
- default — / {}
- Answers by question id, controlled or initial; "" is a skipped optional question. The trail opens at the first unanswered one.
- onValueChange
- (answers) => void
- default —
- Fires from the press, Enter or Skip that changed an answer, with every answer kept.
- onAnswer
- (questionId, value, answers) => void
- default —
- Fires once per answer given.
- onComplete
- (answers, path) => void
- default —
- Fires when the last question on the path is answered, with the path's ids — again after a rewind that completes it.
- onRewind
- (questionId) => void
- default —
- Fires when an earlier answer is opened again.
- onSave
- (saved: { answers, at }) => void
- default —
- Fires from Save for later with the answers and the question waiting.
- storageKey
- string
- default —
- Writes a saved trail to this device's storage under this key and picks it up after mount.
- bubbleStyle
- "soft" | "outline" | "plain"
- default "soft"
- Filled bubbles with a tail corner, hairline bubbles, or a plain transcript on a left rule.
- density
- "compact" | "regular" | "roomy"
- default "regular"
- Gaps, padding and type size.
- accent
- string
- default "var(--accent-bright)"
- Answer chips, the composer's focus, progress and the save mark — any CSS colour.
- typingDelay
- number
- default 420
- How long the host types before a new question arrives, in ms.
- host
- string
- default "Basinworks"
- Who is asking; named to assistive tech before each question.
- title
- string
- default "Book a call-out"
- The intake's heading.
- closing
- string | (answers) => string
- default a thank-you that repeats the phone number
- The host's last line once every question on the path is answered.
- status
- "ready" | "loading" | "error"
- default "ready"
- Loading shows placeholder bubbles; error explains and offers Try again.
- onRetry
- () => void
- default —
- Try again was pressed after the questions failed to load.
- height
- number
- default 520
- Height of the surface in px; the trail scrolls inside it.
- disabled
- boolean
- default false
- Read only: nothing can be answered or changed.
- className
- string
- default —
- Classes for the surface.
| Prop | Type | Default | Description |
|---|---|---|---|
| questions | QuestionTrailQuestion[] | defaultQuestionTrailQuestions | { id, prompt, short?, kind?, options?, placeholder?, optional?, next?, validate? } — next is an id, a map from answer to id (a branch), or null to end. |
| value / defaultValue | Record<string, string> | — / {} | Answers by question id, controlled or initial; "" is a skipped optional question. The trail opens at the first unanswered one. |
| onValueChange | (answers) => void | — | Fires from the press, Enter or Skip that changed an answer, with every answer kept. |
| onAnswer | (questionId, value, answers) => void | — | Fires once per answer given. |
| onComplete | (answers, path) => void | — | Fires when the last question on the path is answered, with the path's ids — again after a rewind that completes it. |
| onRewind | (questionId) => void | — | Fires when an earlier answer is opened again. |
| onSave | (saved: { answers, at }) => void | — | Fires from Save for later with the answers and the question waiting. |
| storageKey | string | — | Writes a saved trail to this device's storage under this key and picks it up after mount. |
| bubbleStyle | "soft" | "outline" | "plain" | "soft" | Filled bubbles with a tail corner, hairline bubbles, or a plain transcript on a left rule. |
| density | "compact" | "regular" | "roomy" | "regular" | Gaps, padding and type size. |
| accent | string | "var(--accent-bright)" | Answer chips, the composer's focus, progress and the save mark — any CSS colour. |
| typingDelay | number | 420 | How long the host types before a new question arrives, in ms. |
| host | string | "Basinworks" | Who is asking; named to assistive tech before each question. |
| title | string | "Book a call-out" | The intake's heading. |
| closing | string | (answers) => string | a thank-you that repeats the phone number | The host's last line once every question on the path is answered. |
| status | "ready" | "loading" | "error" | "ready" | Loading shows placeholder bubbles; error explains and offers Try again. |
| onRetry | () => void | — | Try again was pressed after the questions failed to load. |
| height | number | 520 | Height of the surface in px; the trail scrolls inside it. |
| disabled | boolean | false | Read only: nothing can be answered or changed. |
| className | string | — | Classes for the surface. |