Example
Live demos built with @landbot/core (a property-search assistant, a cinematic landing page, a split-flap board and a self-writing site), plus a minimal React chat.
-
Property-search assistant: a product-style app. An eight-step real-estate assistant (location, property type, bedrooms, budget, timeline, results, contact, goodbye) where the chat drives a custom UI instead of a bubble stream. Built with
@landbot/core, React, Tailwind and shadcn/ui; source inrealtor/at the repo root. The bot tags each step with acustomer.uivalue, and the app renders a React component per step: a slider for budget, a card grid for results, a form for contact. Every prompt gets its own controls while Core owns the conversation state. Of these demos, it's the closest to a real integration. -
Cinematic conversation: a scene-per-message landing page built on
@landbot/core. Each bot reply takes over the full viewport in oversized DM Serif Display, and five visual themes rotate per scene (Midnight, Rose, Cream, Ocean, Electric). Source lives incinematic/at the repo root: three files (index.html,style.css,cinematic.js), about 670 lines in total. It shows@landbot/corepatterns you don't see in a typical chat widget: subscribing to$readableSequencefor paced delivery, filtering user messages out of the render stream with thesamuraifield, a dwell loop scaled to message length with a "Continue ▼" control so readers can pace themselves through bot bursts, and reserved layout space so the message doesn't jump when the input fades in. -
Split-flap departure board: a Solari-style departure board. Each bot reply rolls in character by character across a grid of amber flap tiles, dialog choices become numbered "tracks" (TRACK 01, TRACK 02), and prior messages scroll up the board as faded history rows. Source lives in
departures/at the repo root:index.html,style.cssanddepartures.js. It's built on$typingSequence, so the SDK's typing state drives the UI.state: trueticks flip the status bar toBOARD ACTIVE · INCOMINGwith a faster pulse, and the matchingstate: falsetick triggers the flap roll. It also shows howtype: 'dialog'choices and thepayloadsarray map onto a control that isn't chat (numbered platforms), and how to drop the chat bubble in favor of a single "now showing" display plus a small scrolling log. -
The site that writes itself: a two-pane demo, with a real
@landbot/corechat on the left and a marketing-page mock on the right. Each user reply changes the page in real time. The product name fills the logo and hero, the one-liner becomes the subhead, a brand color answer re-themes every CTA and accent, three feature replies fill the feature cards, and the audience reply lights up the closing block with a celebratory pulse. Source:composer/index.html,style.cssandcomposer.js, plus aBOT_SETUP.mdguide in the same directory.core.pipelines.$readableSequence.subscribe(handleMessage)renders the bot's prompts and counts user replies. Each user reply, in order, advances a local state machine whoseapply()functions change the page (setProductName,setHeadline,setBrandColour,setFeature,setAudience). There's no bot-side Custom JS and no Code block: the bot asks questions in sequence, and the parent page owns the state. By default the demo loads the same sample bot as the other two examples. It works out of the box, because the page changes follow the count of user replies rather than what the bot says, but the prompts won't match the wizard. For the full experience, followcomposer/BOT_SETUP.md(in the repo) to build a wizard bot in the Landbot dashboard, then pass its config URL with?bot=...or swap the fallback incomposer.js. That takes about 20 minutes in the builder and no code changes.
Minimal chat example
The cinematic demo above is a creative remix. The example here is a minimal chat: a React component with no styling commitments that captures the standard @landbot/core wiring. Start here when you're learning the SDK, and borrow from the cinematic version once you want to deviate.
The five lines you'll always write:
- Fetch the bot config from
index.json. - Create a
Coreinstance from the parsed config. - Subscribe to
$readableSequencefor incoming messages. - Call
init()to load the welcome / history. - Use
sendMessageto push user input back to the bot.
You don't have to render it as a chat, because Core is a messaging engine. Once you have the message stream and a way to send, the UI is yours.
Tip Want the same wiring without React or a build step? The cookbook recipe Build a custom chat UI with
@landbot/coreis these five steps in a single copy-paste HTML file, with notes on the patterns to get right (subscribe beforeinit(), discriminate onauthor_type, dedupe onkey).
Source code
import React, { useState, useEffect, useRef } from 'react';
import Core from '@landbot/core';
function parseMessage(data) {
return {
key: data.key,
text: data.title || data.message,
author: data.samurai !== undefined ? 'bot' : 'user',
timestamp: data.timestamp,
type: data.type,
};
}
function parseMessages(messages) {
return Object.values(messages).reduce((obj, next) => {
obj[next.key] = parseMessage(next);
return obj;
}, {});
}
function messagesFilter(data) {
// Render only basic message types in this minimal example
return ['text', 'dialog'].includes(data.type);
}
function scrollBottom(container) {
if (container) {
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth',
});
}
}
export default function Chat() {
const [messages, setMessages] = useState({});
const [input, setInput] = useState('');
const [config, setConfig] = useState(null);
const core = useRef(null);
useEffect(() => {
fetch('https://chats.landbot.io/u/H-441480-B0Q96FP58V53BJ2J/index.json')
.then(res => res.json())
.then(setConfig);
}, []);
useEffect(() => {
if (config) {
core.current = new Core(config);
core.current.pipelines.$readableSequence.subscribe(data => {
setMessages(messages => ({
...messages,
[data.key]: parseMessage(data),
}));
});
core.current.init().then(data => {
setMessages(parseMessages(data.messages));
});
}
}, [config]);
useEffect(() => {
const container = document.getElementById('landbot-messages-container');
scrollBottom(container);
}, [messages]);
const submit = () => {
if (input !== '' && core.current) {
core.current.sendMessage({ message: input });
setInput('');
}
};
return (
<>
<div className="landbot-header">
<h1 className="subtitle">Landbot core example</h1>
</div>
<div
className="landbot-messages-container"
id="landbot-messages-container"
>
{Object.values(messages)
.filter(messagesFilter)
.sort((a, b) => a.timestamp - b.timestamp)
.map(message => (
<article
className="media landbot-message"
data-author={message.author}
key={message.key}
>
<figure className="media-left landbot-message-avatar">
<p className="image is-64x64">
<img
alt=""
className="is-rounded"
src="https://i.pravatar.cc/100"
/>
</p>
</figure>
<div className="media-content landbot-message-content">
<div className="content">
<p>{message.text}</p>
</div>
</div>
</article>
))}
</div>
<div className="landbot-input-container">
<div className="field">
<div className="control">
<input
className="landbot-input"
onChange={e => setInput(e.target.value)}
onKeyUp={e => {
if (e.key === 'Enter') {
e.preventDefault();
submit();
}
}}
placeholder="Type here..."
type="text"
value={input}
/>
<button
className="button landbot-input-send"
disabled={input === ''}
onClick={submit}
type="button"
>
<span className="icon is-large" style={{ fontSize: 25 }}>
➤
</span>
</button>
</div>
</div>
</div>
</>
);
}
What to swap in
- Replace the
fetchURL, which carries theH-441480-...channel ID, with your bot's config URL. - Pick the pipeline that matches the cadence you want:
$sequencefor instant ordered delivery,$readableSequence(used here) for natural pacing, or$typingSequenceif you want to render a typing indicator. - Extend
messagesFilterto render more message types, such asimage,iframe, or custom rendering forhidden.
Next steps
- Landbot.Core API: full method and property reference.
- Messages: every message shape Core accepts and emits.
- Pipelines: the three pacing strategies.
- Events: emitting and subscribing to custom events.