Example

Live, interactive demos built with @landbot/core — a property-search assistant, a cinematic landing page, a split-flap board, and a self-writing site — plus the canonical minimal React chat.

  • See it in action — property-search assistant — A full product-style app: an eight-step real-estate assistant (Location → Property type → Bedrooms → Budget → Timeline → Results → Contact → Goodbye) where the chat drives a rich custom UI instead of a bubble stream. Built with @landbot/core + React + Tailwind + shadcn/ui; source in realtor/ at the repo root. The pattern: the bot tags each step with a customer.ui value and the app dispatches to a bespoke React component per step — a slider for budget, a card grid for results, a form for contact — so every prompt gets purpose-built controls while Core owns the conversation state. The closest of these demos to a real integration.

  • See it in action — 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; five visual themes rotate per scene (Midnight, Rose, Cream, Ocean, Electric). Source lives in cinematic/ at the repo root — three files (index.html, style.css, cinematic.js) totalling ~670 lines. Demonstrates @landbot/core patterns you don't see in a typical chat widget: subscribing to $readableSequence for paced delivery, filtering user messages out of the render stream via the samurai field, a length-scaled dwell loop with a "Continue ▼" affordance so readers can pace themselves through bot bursts, and reserved layout space so the message doesn't jump when the input fades in.

  • See it in action — split-flap departure board — A Solari-style departure board where 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 — three files (index.html, style.css, departures.js). This one is built on $typingSequence specifically, so the SDK's typing state actually drives the UI — state: true ticks flip the status bar to BOARD ACTIVE · INCOMING with a faster pulse, and the matching state: false tick triggers the flap roll. Also shows how type: 'dialog' choices and the payloads array map naturally onto a non-chat affordance (numbered platforms), and how to throw away the chat-bubble metaphor entirely in favor of a single in-place "now showing" display plus a small scrolling log.

  • See it in action — the site that writes itself — A dual-pane demo: real @landbot/core chat on the left, a marketing-page mock on the right. Each user reply mutates the page in real time — product name fills the logo and hero, the one-liner becomes the subhead, a brand-colour answer re-themes every CTA and accent, three feature replies populate the feature cards, the audience reply lights up the closing block and fires a celebratory pulse. Source: composer/index.html, style.css, composer.js — plus a BOT_SETUP.md guide in the same directory. The pattern: core.pipelines.$readableSequence.subscribe(handleMessage) renders the bot's prompts and counts user replies; each user reply (in order) advances a local state machine whose apply() functions mutate the page (setProductName, setHeadline, setBrandColour, setFeature, setAudience). No bot-side Custom JS, no Code blocks — just bot questions in sequence and a parent that owns the state. Chat is the controller, the page is the canvas. A note on the default config: the demo loads against the same sample bot the other two examples use, which means it works out of the box — page mutations fire correctly because they're driven by user-reply count, not by what the bot says — but the prompts won't match the wizard's intent. For the coherent experience, follow composer/BOT_SETUP.md (in the repo) to build a wizard bot in the Landbot dashboard, then pass its config URL via ?bot=... or swap the fallback in composer.js. About 20 minutes of bot-builder work; no code changes.

Minimal chat example

The cinematic demo above is a creative remix. The example here is the canonical minimal chat — a React component with no styling commitments that captures just the standard @landbot/core wiring. Start here when you're learning the SDK; copy ideas from the cinematic version once you want to deviate.

The five lines you'll always write:

  1. Fetch the bot config from index.json.
  2. Create a Core instance from the parsed config.
  3. Subscribe to $readableSequence for incoming messages.
  4. Call init() to load the welcome / history.
  5. Use sendMessage to push user input back to the bot.

You don't have to render it as a chat — Core is just 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/core is these five steps in a single copy-paste HTML file, with notes on the patterns to get right (subscribe before init(), discriminate on author_type, dedupe on key).

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 H-441480-... bot ID in the fetch URL with your bot's config URL.
  • Pick the pipeline that matches the cadence you want — $sequence for instant ordered delivery, $readableSequence (used here) for natural pacing, or $typingSequence if you want to render a typing indicator.
  • Extend messagesFilter to render more message typesimage, iframe, custom rendering for hidden, etc.

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.