How to build a chat bubble contact form in React
A chat bubble contact form in React replaces a traditional form with a sequential conversation: each question appears as a bot bubble animated with Framer Motion, the user types a reply and presses Enter, and the next question slides in. State tracks the current step index and an array of collected answers.
- Stack: React 18 + Framer Motion 11 + lucide-react, ~155 lines, zero extra dependencies.
- Animation: each BotBubble mounts with opacity/y/scale via AnimatePresence; UserBubble mirrors the same spring.
- Supports text, email and textarea input types per step, configured via the steps prop.
- Accessible: semantic input elements, keyboard submit on Enter (Shift+Enter inserts a newline in textarea).
- Fully responsive out of the box; the card caps at 600px centered and uses CSS tokens for spacing.
Contact Chat Bubbles turns a dull multi-field form into a back-and-forth conversation. The bot asks one question at a time as a left-aligned bubble; the user's answer floats to the right. It removes the visual overhead of a traditional form and makes the act of reaching out feel lighter, especially for creative agencies and SaaS products targeting a non-technical audience.
Anatomy
The component is a single centered card capped at 600px. The card has three zones stacked vertically: a header bar with the bot name and an online indicator, a scrollable message area with a minimum height of 320px, and an input row at the bottom. Bot bubbles sit on the left with a 32px icon avatar; user replies are right-aligned accent-colored bubbles with a flipped border-radius corner. The input row disappears once all steps are complete, replaced by a final confirmation bubble.
How it works
The key state is two values: `currentStep` (an integer) and `answers` (a string array). When the user submits, the input value is pushed into answers and currentStep increments. The message area renders `steps.slice(0, currentStep + 1)`, so all previous questions and answers stay visible. Each BotBubble receives a `delay` prop: steps that have already been answered mount with delay 0 (they are restoring, not revealing), while the current new step gets a 0.3s delay to feel like the bot is typing. AnimatePresence wraps the list with mode='sync' so new bubbles mount smoothly without unmounting existing ones.
How to build it in React
Define the steps array
Each step is a plain object with a question string, an inputType ('text' | 'email' | 'textarea') and a placeholder. Pass the array as a prop so the chat script stays outside the component and is easy to translate or A/B test.
const steps = [ { question: "What's your name?", inputType: "text", placeholder: "Jane Doe" }, { question: "Your email?", inputType: "email", placeholder: "[email protected]" }, { question: "How can we help?", inputType: "textarea", placeholder: "Tell us…" }, ];Manage step state
Keep currentStep and answers in useState. The submit handler validates that inputValue is not empty, appends it to answers, clears the field and increments the step. A single isComplete flag derived from currentStep >= steps.length drives what renders.
const [currentStep, setCurrentStep] = useState(0); const [answers, setAnswers] = useState<string[]>([]); const handleSubmit = () => { if (!inputValue.trim()) return; setAnswers(prev => [...prev, inputValue.trim()]); setInputValue(""); setCurrentStep(prev => prev + 1); };Animate bot bubbles with a stagger delay
The BotBubble component accepts a delay prop. Pass `delay={0}` for already-seen steps (they appear instantly on re-render) and `delay={0.3}` for the freshly revealed question. This tiny distinction makes the chat feel live without a fake typing indicator.
<BotBubble text={step.question} delay={i === currentStep && answers.length === i ? 0.3 : 0} />Handle keyboard submit
For text and email inputs, attach `onKeyDown` to call handleSubmit on Enter. For textarea, also check `!e.shiftKey` so the user can add newlines with Shift+Enter before sending. Call `e.preventDefault()` on Enter in the textarea to avoid form submission or extra newlines.
onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSubmit(); } }}
When to use it
Reach for this pattern when the contact form is a primary conversion point and you want to lower perceived friction, such as an agency quote request, a SaaS trial signup, or a creative studio introduction. The sequential nature keeps the user focused on one question at a time. Avoid it when the form has more than six or seven steps (it becomes a chore), when fields need to be edited after submission, or when you need server-side validation per step with real-time error messages, the current implementation collects answers client-side and sends them all at once.
Used by
- Typeform, Popularized the one-question-at-a-time conversational form pattern that this component is inspired by.
- Intercom, Uses chat bubble UI flows for lead qualification and support routing directly on client websites.
- Drift, Built its entire product around conversational forms embedded as chat widgets on landing pages.
FAQ
How do I send the collected answers to my backend?
When isComplete becomes true, trigger a fetch or axios call inside a useEffect that watches currentStep. Map answers to your steps array to rebuild a named payload before sending.
Can I validate each answer before advancing to the next step?
Yes. Add a `validate` function to each step object. In handleSubmit, run validate(inputValue) and if it returns an error string, write it to a local error state and bail out before incrementing currentStep.
Why does the bot bubble use a stagger delay prop instead of CSS animation-delay?
A JS-side delay prop through Framer Motion integrates with AnimatePresence, so the delay only fires on initial mount, not on re-renders caused by parent state changes. CSS animation-delay would replay on every render cycle.
Does it work on mobile?
The layout is fully responsive and the card fills its container on small screens. On iOS, tap the input to open the keyboard; the Send button is 44px square to meet touch-target guidelines. There is no dependency on pointer events, so touch works without any special handling.