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:
- 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.
- Anchor-preserve — when you prepend older history, the message they were reading stays exactly where it is instead of jumping.
- 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.
Streaming footer
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.
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"witharia-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:streamswap 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()respectprefers-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
scrollTopfrom your own code fights the pin. UsescrollToBottom()/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 |