Skip to main content
WireKit
Copy for LLM

Conversation

The scroll container a live chat needs. A plain overflow-y-auto div gets a conversation wrong in three ways — Conversation fixes all three:

  1. Follow-output — it pins the viewport to the newest message as messages (or streamed tokens) arrive, but only while the reader is already at the bottom. The moment they scroll up to read back, the pin releases, so their position is never yanked away mid-sentence.
  2. Anchor-preserve — when you prepend older history, the message they were reading stays exactly where it is instead of jumping.
  3. Jump-to-latest — while they are scrolled away, an unread counter appears; one click returns them to the newest message.

It watches the DOM, not a JavaScript data source — so wire:poll, wire:stream, and Echo broadcasts all drive it with no extra wiring. Pair it with Message for the rows.

Basic Usage

Give it messages and a height. That is the whole setup — follow-output is on.

A chat transcript
Ada
Morning — is the export API rate-limited?
You
Yes, 60 requests a minute per token.
Ada
Perfect. Does a 429 include a Retry-After header?
You
It does — always in seconds.
Ada
That is everything I needed, thanks!
You
Any time.

The footer slot is pinned to the end of the transcript and participates in follow-output — so a typing indicator stays glued to the bottom while the reader is following along.

A pinned streaming footer
You
Summarize the release notes.
Assistant
Sure — pulling the last three versions now.
Loading

Assistant is thinking…

Loading older history

When the reader scrolls near the top, Conversation dispatches conversation-reached-top. Handle it with x-on → a Livewire method; the scroller preserves the reader's position once your history lands.

{{-- 1. loadOlder() prepends messages; the scroller keeps the reader in place --}}
<x-wirekit::conversation
    @conversation-reached-top="$wire.loadOlder()"
    max-height="24rem"
>
    {{-- 2. A spinner while the prepend is in flight --}}
    <div wire:loading wire:target="loadOlder">
        <x-wirekit::spinner size="sm" />
    </div>

    @foreach($messages as $message)
        <x-wirekit::message :author="$message->author" :timestamp="$message->created_at">
            {{ $message->body }}
        </x-wirekit::message>
    @endforeach
</x-wirekit::conversation>

Livewire streaming

Because the scroller watches the DOM, wire:stream needs no extra wiring — the tokens land, the transcript follows:

{{-- 1. Streamed tokens append into this bubble; follow-output tracks them --}}
<x-wirekit::conversation max-height="24rem">
    <x-wirekit::message :author="['name' => 'Assistant']">
        <span wire:stream="answer">{{ $answer }}</span>
    </x-wirekit::message>
</x-wirekit::conversation>

Deep-linking to a message

Tag rows with data-wk-message-id and call scrollToMessage(id) to jump to a quoted message:

{{-- 1. Tag the row --}}
<x-wirekit::message data-wk-message-id="{{ $message->id }}">…</x-wirekit::message>

{{-- 2. Jump to it from a reply-quote --}}
<button type="button" @click="scrollToMessage('{{ $quoted->id }}')">View quote</button>

Props

Prop Type Default Description
label string 'Conversation' Accessible name of the transcript log region
maxHeight string '24rem' Height cap of the scroll viewport — a transcript needs a bounded height
threshold int|null null px tolerance for "at bottom" (plugin default 24)
scope string|null null Scoped personalization name

Slots

Slot Description
default The message rows
footer Pinned to the end of the transcript; participates in follow-output (typing indicator)

Events

Event When
conversation-reached-top The reader scrolled near the top — load older history

Accessibility

  • The viewport is role="log" with aria-live="polite" — the semantic role for a transcript, so new messages are announced politely without interrupting.
  • The live region is always present in the DOM. A region that appears at the same moment its text does is inert to assistive technology, so a wire:stream swap inside a conditionally-rendered region would never announce. This one is rendered up front.
  • The viewport carries tabindex="0" + aria-label, so the scroll region is reachable and operable by keyboard (WCAG 2.1.1), with a visible focus ring.
  • The jump-to-latest control is a real <button> and announces the unread count ("Jump to latest, 3 new") — not just "there is more".
  • scrollToBottom() / scrollToMessage() respect prefers-reduced-motion: they jump instantly instead of smooth-scrolling.

Keyboard Interaction

Key Action
Tab Focus the transcript region
/ Scroll the transcript (native)
Page Up / Page Down Scroll by a viewport (native)
Home / End Jump to the start / end (native)

Pitfalls

  • Give it a height. Without a maxHeight (or a height class) there is nothing to scroll and follow-output has no meaning.
  • Do not scroll it yourself. Calling scrollTop from your own code fights the pin. Use scrollToBottom() / scrollToMessage().
  • Prepend, do not replace, for history. Replacing the whole list drops the anchor and the reader loses their place.

Design Tokens

Element Token
Scrollbar --color-wk-scrollbar-* (via .wk-scrollbar)
Focus ring --color-wk-ring
Jump control shadow --shadow-wk-md
Jump control offset --space-wk-sm

Further Reading

Was this page helpful?

Voting requires cookies or local storage. What we store