|
| 1 | +# Svelte Virtual Chat - Complete Reference |
| 2 | + |
| 3 | +> A high-performance virtual chat viewport for Svelte 5. Purpose-built for LLM conversations, support chat, and any message-based UI with follow-bottom, streaming stability, and history prepend with anchor preservation. |
| 4 | + |
| 5 | +## Package Information |
| 6 | + |
| 7 | +- Package: `@humanspeak/svelte-virtual-chat` |
| 8 | +- License: MIT |
| 9 | +- Requires: Svelte 5 |
| 10 | +- Dependencies: esm-env (SSR detection only) |
| 11 | +- Maintained by: Humanspeak, Inc. |
| 12 | +- Homepage: https://virtualchat.svelte.page |
| 13 | +- Repository: https://github.com/humanspeak/svelte-virtual-chat |
| 14 | + |
| 15 | +## Installation |
| 16 | + |
| 17 | +```bash |
| 18 | +pnpm add @humanspeak/svelte-virtual-chat |
| 19 | +# or |
| 20 | +npm install @humanspeak/svelte-virtual-chat |
| 21 | +``` |
| 22 | + |
| 23 | +## SvelteVirtualChat Component Props |
| 24 | + |
| 25 | +| Prop | Type | Default | Description | |
| 26 | +|------|------|---------|-------------| |
| 27 | +| messages | TMessage[] | Required | Array of messages in chronological order (oldest first) | |
| 28 | +| getMessageId | (msg: TMessage) => string | Required | Extract a unique, stable ID from a message | |
| 29 | +| renderMessage | Snippet<[TMessage, number]> | Required | Svelte 5 snippet that renders a single message | |
| 30 | +| estimatedMessageHeight | number | 72 | Height estimate in pixels for unmeasured messages | |
| 31 | +| followBottomThresholdPx | number | 48 | Distance from bottom to consider "at bottom" | |
| 32 | +| overscan | number | 6 | Extra messages rendered above/below the viewport | |
| 33 | +| onNeedHistory | () => void \| Promise<void> | - | Called when user scrolls near top | |
| 34 | +| onFollowBottomChange | (isFollowing: boolean) => void | - | Called when follow-bottom state changes | |
| 35 | +| onDebugInfo | (info: SvelteVirtualChatDebugInfo) => void | - | Called with live stats on every scroll/render | |
| 36 | +| containerClass | string | '' | CSS class for outermost container | |
| 37 | +| viewportClass | string | '' | CSS class for scrollable viewport | |
| 38 | +| debug | boolean | false | Reserved for future use | |
| 39 | +| testId | string | - | Base test ID for data-testid attributes | |
| 40 | + |
| 41 | +## Basic Usage |
| 42 | + |
| 43 | +```svelte |
| 44 | +<script lang="ts"> |
| 45 | + import SvelteVirtualChat from '@humanspeak/svelte-virtual-chat' |
| 46 | + |
| 47 | + type Message = { id: string; role: string; content: string } |
| 48 | + |
| 49 | + let messages: Message[] = $state([ |
| 50 | + { id: '1', role: 'assistant', content: 'Hello!' }, |
| 51 | + { id: '2', role: 'user', content: 'Hi there.' } |
| 52 | + ]) |
| 53 | +</script> |
| 54 | + |
| 55 | +<div class="h-[600px]"> |
| 56 | + <SvelteVirtualChat |
| 57 | + {messages} |
| 58 | + getMessageId={(msg) => msg.id} |
| 59 | + estimatedMessageHeight={72} |
| 60 | + containerClass="h-full" |
| 61 | + viewportClass="h-full" |
| 62 | + > |
| 63 | + {#snippet renderMessage(message, index)} |
| 64 | + <div class="p-4 border-b"> |
| 65 | + <strong>{message.role}</strong> |
| 66 | + <p>{message.content}</p> |
| 67 | + </div> |
| 68 | + {/snippet} |
| 69 | + </SvelteVirtualChat> |
| 70 | +</div> |
| 71 | +``` |
| 72 | + |
| 73 | +## Imperative API (via bind:this) |
| 74 | + |
| 75 | +```typescript |
| 76 | +let chat: ReturnType<typeof SvelteVirtualChat> |
| 77 | +``` |
| 78 | + |
| 79 | +| Method | Signature | Description | |
| 80 | +|--------|-----------|-------------| |
| 81 | +| scrollToBottom | (options?: { smooth?: boolean }) => void | Scroll viewport to bottom | |
| 82 | +| scrollToMessage | (id: string, options?: { smooth?: boolean }) => void | Scroll to specific message | |
| 83 | +| isAtBottom | () => boolean | Check if following bottom | |
| 84 | +| getDebugInfo | () => SvelteVirtualChatDebugInfo | Get current state snapshot | |
| 85 | + |
| 86 | +## SvelteVirtualChatDebugInfo |
| 87 | + |
| 88 | +| Field | Type | Description | |
| 89 | +|-------|------|-------------| |
| 90 | +| totalMessages | number | Total messages in array | |
| 91 | +| renderedCount | number | Messages currently in DOM | |
| 92 | +| measuredCount | number | Messages with measured heights | |
| 93 | +| startIndex | number | First rendered index | |
| 94 | +| endIndex | number | Last rendered index | |
| 95 | +| totalHeight | number | Calculated content height (px) | |
| 96 | +| scrollTop | number | Current scroll position (px) | |
| 97 | +| viewportHeight | number | Viewport height (px) | |
| 98 | +| isFollowingBottom | boolean | Whether viewport is pinned to bottom | |
| 99 | +| averageHeight | number | Average measured message height (px) | |
| 100 | + |
| 101 | +## How Virtualization Works |
| 102 | + |
| 103 | +1. Height caching β each message height measured via ResizeObserver, cached by ID |
| 104 | +2. Visible range β on every scroll, calculates which messages fall within viewport + overscan |
| 105 | +3. Absolute positioning β only visible messages rendered via transform: translateY() |
| 106 | +4. Follow-bottom β when at bottom, new messages and height changes snap to scrollHeight |
| 107 | +5. Bottom gravity β when messages don't fill viewport, flex justify-content: flex-end pushes to bottom |
| 108 | + |
| 109 | +With 10,000 messages, the DOM contains ~15-25 elements. |
| 110 | + |
| 111 | +## LLM Streaming |
| 112 | + |
| 113 | +As a message grows token by token, ResizeObserver detects height changes. Height corrections are batched per requestAnimationFrame. The viewport stays pinned to bottom automatically. |
| 114 | + |
| 115 | +```typescript |
| 116 | +// Just mutate the message content β ResizeObserver handles the rest |
| 117 | +const msg = messages.find(m => m.id === streamingId) |
| 118 | +if (msg) msg.content += chunk |
| 119 | +``` |
| 120 | + |
| 121 | +Pair with @humanspeak/svelte-markdown for rich markdown rendering: |
| 122 | + |
| 123 | +```svelte |
| 124 | +<SvelteMarkdown source={message.content} streaming={message.isStreaming ?? false} /> |
| 125 | +``` |
| 126 | + |
| 127 | +## History Loading |
| 128 | + |
| 129 | +Use onNeedHistory to load older messages when the user scrolls near the top: |
| 130 | + |
| 131 | +```svelte |
| 132 | +<SvelteVirtualChat |
| 133 | + {messages} |
| 134 | + getMessageId={(msg) => msg.id} |
| 135 | + onNeedHistory={loadOlderMessages} |
| 136 | + ... |
| 137 | +/> |
| 138 | +``` |
| 139 | + |
| 140 | +Prepend older messages: `messages = [...older, ...messages]` |
| 141 | +The component preserves scroll position during prepend operations. |
| 142 | + |
| 143 | +## Scroll Behavior |
| 144 | + |
| 145 | +Two states: following bottom and scrolled away. |
| 146 | + |
| 147 | +- Following: viewport within followBottomThresholdPx of bottom. New messages auto-scroll. |
| 148 | +- Scrolled away: user scrolled up past threshold. New messages do not move viewport. |
| 149 | + |
| 150 | +Use onFollowBottomChange to show "new messages" indicators: |
| 151 | + |
| 152 | +```svelte |
| 153 | +<SvelteVirtualChat |
| 154 | + onFollowBottomChange={(following) => { |
| 155 | + isFollowing = following |
| 156 | + if (following) unreadCount = 0 |
| 157 | + }} |
| 158 | + ... |
| 159 | +/> |
| 160 | +``` |
| 161 | + |
| 162 | +## Height Constraint |
| 163 | + |
| 164 | +The component MUST have a defined height. The parent needs a fixed or flex-derived height: |
| 165 | + |
| 166 | +```svelte |
| 167 | +<div class="h-screen flex flex-col"> |
| 168 | + <header>...</header> |
| 169 | + <div class="flex-1 min-h-0"> |
| 170 | + <SvelteVirtualChat containerClass="h-full" viewportClass="h-full" ... /> |
| 171 | + </div> |
| 172 | +</div> |
| 173 | +``` |
| 174 | + |
| 175 | +## Exported Types |
| 176 | + |
| 177 | +- SvelteVirtualChatProps β component props type (generic over TMessage) |
| 178 | +- SvelteVirtualChatDebugInfo β debug info snapshot type |
| 179 | +- ScrollToBottomOptions β options for scrollToBottom |
| 180 | +- ScrollToMessageOptions β options for scrollToMessage |
| 181 | +- VisibleRange β visible range result type |
| 182 | +- ScrollAnchor β scroll anchor for history prepend |
| 183 | + |
| 184 | +## Exported Utilities |
| 185 | + |
| 186 | +- ChatHeightCache β reactive height cache class |
| 187 | +- captureScrollAnchor β capture scroll position before prepend |
| 188 | +- restoreScrollAnchor β restore scroll position after prepend |
| 189 | + |
| 190 | +## Companion Libraries |
| 191 | + |
| 192 | +- @humanspeak/svelte-markdown β Markdown renderer with LLM streaming mode (~1.6ms per update) |
| 193 | +- @humanspeak/svelte-virtual-list β General-purpose virtual list for non-chat use cases |
| 194 | + |
| 195 | +## Documentation Links |
| 196 | + |
| 197 | +- [Getting Started](https://virtualchat.svelte.page/docs/getting-started) |
| 198 | +- [SvelteVirtualChat API](https://virtualchat.svelte.page/docs/api/svelte-virtual-chat) |
| 199 | +- [Props Reference](https://virtualchat.svelte.page/docs/api/props) |
| 200 | +- [Imperative API](https://virtualchat.svelte.page/docs/api/imperative) |
| 201 | +- [LLM Streaming Guide](https://virtualchat.svelte.page/docs/guides/llm-streaming) |
| 202 | +- [History Loading Guide](https://virtualchat.svelte.page/docs/guides/history-loading) |
| 203 | +- [Scroll Behavior Guide](https://virtualchat.svelte.page/docs/guides/scroll-behavior) |
| 204 | +- [Interactive Examples](https://virtualchat.svelte.page/examples) |
0 commit comments