[{"data":1,"prerenderedAt":4},["ShallowReactive",2],{"article-doc:docs\u002Fguides\u002F3.stable-caption-ui":3},"---\ntitle: Build a Stable Caption UI\ndescription: Consume StableStream events without manufacturing caption jitter.\n---\nHow to consume realtime transcription events and build a caption UI that doesn't jitter.\n\n## The problem with naive approaches\n\nA naive live caption UI might try to update text on every event. With partial-text systems, this causes visible jitter as words are repeatedly replaced.\n\nLansonAI's StableStream eliminates this: each `conversation.item.input_audio_transcription.completed` event contains final, stable text.\n\n## Recommended render strategy\n\n### Append-only display\n\n```typescript\nconst captions = document.getElementById('captions');\n\nws.onmessage = (event) => {\n  const data = JSON.parse(event.data);\n\n  if (data.type === 'conversation.item.input_audio_transcription.completed') {\n    \u002F\u002F Append directly — text is final, no replacement needed\n    const p = document.createElement('p');\n    p.dataset.utterance = data.utterance_index;\n    p.textContent = data.text;\n    captions.appendChild(p);\n    captions.scrollTop = captions.scrollHeight;\n  }\n};\n```\n\n### With optional status indicators\n\n```typescript\nws.onmessage = (event) => {\n  const data = JSON.parse(event.data);\n\n  if (data.type === 'input_audio_buffer.speech_started') {\n    showIndicator(data.utterance_index, 'listening');\n  }\n\n  if (data.type === 'input_audio_buffer.speech_stopped') {\n    showIndicator(data.utterance_index, 'processing');\n  }\n\n  if (data.type === 'conversation.item.input_audio_transcription.completed') {\n    hideIndicator(data.utterance_index);\n    appendCaption(data.utterance_index, data.text);\n  }\n};\n```\n\n## What you do NOT need to do\n\n- ❌ No partial → final text replacement mapping\n- ❌ No waiting for \"stabilization\" (received text is already stable)\n- ❌ No text rollback or undo\n- ❌ No debounce or jitter elimination\n- ❌ No re-rendering of previously shown text\n\n## Scrolling behavior\n\nFor long sessions, manage scroll behavior:\n\n```typescript\nfunction appendCaption(index, text) {\n  const p = document.createElement('p');\n  p.textContent = text;\n\n  \u002F\u002F Auto-scroll if user is near bottom\n  const isNearBottom =\n    captions.scrollHeight - captions.scrollTop - captions.clientHeight \u003C 100;\n\n  captions.appendChild(p);\n\n  if (isNearBottom) {\n    captions.scrollTop = captions.scrollHeight;\n  }\n}\n```\n\n## Styling for readability\n\n```css\n#captions p {\n  margin: 0.25em 0;\n  padding: 0.25em 0.5em;\n  border-radius: 4px;\n  transition: opacity 0.2s;\n}\n```\n\n## Related\n\n- [StableStream](\u002Fdocs\u002Fconcepts\u002Fstablestream) — the stability contract\n- [Stable vs. Partial Text](\u002Fdocs\u002Fconcepts\u002Fstable-vs-partial-text) — concept\n- [Browser Live Captions](\u002Fdocs\u002Fguides) — full browser setup\n",1790059118951]