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 in realtor/ at the repo root. The bot tags each step with a customer.ui value, 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 in cinematic/ at the repo root: three files (index.html, style.css, cinematic.js), about 670 lines in total. It shows @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 with the samurai field, 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.css and departures.js. It's built on $typingSequence, so the SDK's typing state 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. It also shows how type: 'dialog' choices and the payloads array 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/core chat 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.css and composer.js, plus a BOT_SETUP.md guide 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 whose apply() 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, follow composer/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 in composer.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:

  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, 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/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 fetch URL, which carries the H-441480-... channel ID, 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 types, such as image, iframe, or custom rendering for hidden.

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.