Skip to content
LofiStack Gallery
Type: formWeek 04

Conversational Form

Turns a boring multi-field form into a conversation. The bot asks one question, shows a typing indicator, and your answer appears as a chat bubble. Questions can use earlier answers (“Nice to meet you, Antu”) or branch on them. It supports text, email, number, multi-line and choice-chip steps. Optional steps get a Skip button, there's a progress bar, and you can go back. At the end, a review card lets you edit any answer before sending.

View on GitHub

Start a project

1 of 5
Hi! I'm Lofi. What should I call you?

Answers are passed to onComplete when you send.

Usage

usage.tsx
import { ConversationalForm } from "@/components/gallery/conversational-form";

<ConversationalForm
  title="Start a project"
  steps={[
    { id: "name", label: "Name", question: "Hi! What should I call you?" },
    { id: "email", label: "Email", type: "email", question: (a) => `Nice to meet you, ${a.name}. Your email?` },
    { id: "role", label: "Role", type: "choice", question: "Which sounds like you?", options: ["Designer", "Developer", "Founder"] },
    { id: "message", label: "Message", type: "textarea", required: false, question: "Anything else?" },
  ]}
  onComplete={(answers) => fetch("/api/lead", { method: "POST", body: JSON.stringify(answers) })}
  doneMessage={(a) => `Thanks ${a.name}! We'll reply to ${a.email}.`}
/>

States

Asking (text / email / number / textarea) · Choice chips · Typing indicator · Validation error · Optional step with Skip · Going back to edit · Review summary with Edit · Sending (spinner) · Done + Start over

Start a project

1 of 5
Hi! I'm Lofi. What should I call you?
First question

Start a project

3 of 5
Hi! I'm Lofi. What should I call you?

Antu

Nice to meet you, Antu. What's the best email to reach you?

antu@studio.co

Which of these sounds most like you?
Choice step (mid-way)

Start a project

5 of 5
Hi! I'm Lofi. What should I call you?

Antu

Nice to meet you, Antu. What's the best email to reach you?

antu@studio.co

Which of these sounds most like you?

Founder

What budget are you working with? (USD)

5000

Anything else we should know? (optional)

Skipped

Here's what I've got. Anything to change?

Name
Antu
Email
antu@studio.co
Role
Founder
Budget
5000
Message
—
Review before sending

Props

  • steps*

    ConversationalStep[]

    { id, question, label?, type?, options?, placeholder?, required?, validate? }. question can be a function of earlier answers.

  • onComplete*

    (answers) => void | Promise

    Called from the review step. A promise shows “Sending…”, and a rejection shows an error so you can retry.

  • title= "Let's get started"

    string

    Header title and accessible name.

  • avatar= "✦"

    ReactNode

    Bot avatar in the bubbles.

  • doneMessage

    string | (answers) => string

    Final bot message after sending.

  • typingDelay= 650

    number

    Milliseconds of “typing…” before each question. 0 disables it.

  • initialAnswers

    Record<string, string>

    Resume a conversation. It starts at the first unanswered step.

  • maxHeight= 360

    number

    Max height of the chat log in px. It scrolls after that.

  • className

    string

    Extra classes for the card.

Accessibility

  • The message area is a role="log" live region, so new questions are read aloud.
  • The input is labelled by the current question (aria-labelledby).
  • Errors set aria-invalid, are linked with aria-describedby, and are announced with role="alert".
  • Choice chips are a labelled group of real buttons.
  • The progress bar has role="progressbar" with aria-valuenow.
  • The input doesn't grab focus on page load, only after you start answering. Motion is disabled under prefers-reduced-motion.

Build prompt

The final prompt used to build this component
Build a reusable React 19 + TypeScript + Tailwind v4 component called ConversationalForm: a form that asks one question at a time like a chat.

Props (typed): steps (ConversationalStep = { id, label?, question: string | (answers) => string, type?: "text" | "email" | "number" | "textarea" | "choice", options?, placeholder?, required? (default true), validate?: (value, answers) => string | null }), onComplete(answers) (may return a promise), title, avatar, doneMessage (string or function), typingDelay, initialAnswers, maxHeight, className.

Flow:
- State: answers, current index, status ("asking" | "review" | "sending" | "done"), typing, draft and error. Start at the first unanswered step (supports initialAnswers).
- On answer: validate (required, email regex, numeric, custom validate) and show the error inline. Otherwise save, show the "typing…" dots for typingDelay ms, then move to the next unanswered step, or to the review if none are left. Questions can use earlier answers (personalised or branching copy).
- "← Back" un-answers the previous step and prefills the draft. Optional steps show "Skip" and are saved as an empty answer, shown as "Skipped".
- Review: a bot bubble with a definition list of every answer (label: value) and an Edit button per row. Edit jumps to that step and then returns to the review. "Looks good, send it" calls onComplete. A promise shows a spinner and "Sending…", and a rejection shows an error. Done shows doneMessage and "Start over".

UI: card with title, "N of M" and a progress bar; a scrolling chat log (bot bubbles left with avatar, user bubbles right in indigo, slide-in keyframe, auto-scroll to bottom); a composer with input/textarea + round send button (Enter sends, Shift+Enter makes a new line); choice steps render chips instead.

Accessibility: role="log" aria-live, input aria-labelledby the question, aria-invalid + aria-describedby + role="alert" errors, labelled chip group, progressbar role, no autofocus on page load (only after interaction), reduced-motion support, dark mode. No dependencies.