---
title: Stream
description: Streaming text output (SSE) with the accessibility, reduced-motion, and error handling done right
visibility: guest
draft: false
---

# Stream

The `<x-wirekit::stream>` component renders streaming text output — an LLM response,
a live log — from a Server-Sent Events endpoint. It exists so you never re-write the
three parts that are genuinely hard to get right:

- **Accessibility.** A growing `aria-live="polite"` field is unusable — a screen
  reader re-reads the whole thing on every token. Stream announces that a response is
  *generating* once, then the *result* once, and keeps the visible output out of the
  live region so it never floods.
- **Reduced motion.** A token-by-token build *is* motion. Under
  `prefers-reduced-motion: reduce` the tokens are buffered and revealed as one block.
- **Abort & error.** A half-streamed response whose connection drops has a defined
  terminal state (`aborted` / `failed`), not a silent freeze.

You supply the URL and render the text; the library owns the state machine.

## Usage

Point `url` at an SSE endpoint that emits text tokens and closes with a done signal
(`[DONE]` by default). The stream starts on load and appends tokens as they arrive.

```blade
<x-wirekit::stream url="/chat/{{ $conversation->id }}/stream" />
```

The demos on this page use `simulate` — they type a fixed string out token by token
from a local timer, with no live endpoint, so you can watch the streaming behavior
right here. Use the **↻ Replay** control to run it again. In your app you pass `url`
instead; `simulate` is for demos and typewriter effects.

:::preview{title="Watch it stream"}
<x-wirekit::stream
    simulate="WireKit is a free, MIT-licensed UI component library for Laravel Livewire. It ships accessible, token-themed components you compose with Blade."
/>
:::

## With controls

The default slot renders inside the component's Alpine scope, so you can add Stop and
Retry controls that call `stop()` / `restart()` and react to `status`. Bind your
control buttons with WireKit's own components.

:::preview{title="Stream with stop / retry controls"}
<x-wirekit::stream
    simulate="The quarterly report is ready. Revenue grew 12 percent quarter over quarter, driven by the new self-serve tier."
>
    <x-wirekit::row gap="sm">
        <x-wirekit::button size="sm" intent="neutral" surface="outline" x-show="isStreaming" x-on:click="stop()">Stop</x-wirekit::button>
        <x-wirekit::button size="sm" intent="neutral" surface="outline" x-show="isTerminal" x-on:click="restart()">Regenerate</x-wirekit::button>
    </x-wirekit::row>
</x-wirekit::stream>
:::

## Announcement mode

By default (`announce="result"`) the settled response is announced once in full. For
long outputs where reading the whole thing aloud is too much, use `announce="status"`
to announce only that the response is ready — the text stays on screen for the reader
to navigate to.

:::preview{title="Status-only announcement"}
<x-wirekit::stream
    announce="status"
    simulate="A long generated report would render here; assistive tech is told only that it is ready, not read the whole thing aloud."
/>
:::

## Seeded output (SSR / resume)

Pass `initialText` to render a completed response immediately — resume a finished
conversation on a server render, or show a static example — without opening a stream.
Combine it with `:auto-start="false"` so the component stays idle and simply displays
the seeded text.

:::preview{title="A seeded, already-complete response"}
<x-wirekit::stream
    :auto-start="false"
    initial-text="This response was rendered on the server and seeded into the component — no stream was opened. Reach for this to resume a finished conversation."
/>
:::

## Livewire integration

In your app, point `url` at a Livewire-driven route that returns `text/event-stream`
and writes tokens as your model produces them; drive the trigger from Livewire and let
the component own the rendering and accessibility:

```blade
{{-- 1. A route that streams tokens as Server-Sent Events, ending with [DONE]. --}}
<x-wirekit::stream url="/chat/{{ $conversation->id }}/stream" />
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `url` | `string\|null` | `null` | SSE endpoint to stream from. Null → the component stays idle. |
| `eventName` | `string` | `'message'` | SSE event name to listen for. |
| `doneSignal` | `string` | `'[DONE]'` | Payload that ends the stream. |
| `announce` | `string` | `'result'` | `'result'` announces the final text once; `'status'` announces only readiness. |
| `autoStart` | `bool` | `true` | Open the stream on init. `false` → start it from your own control. |
| `initialText` | `string\|null` | `null` | Seed text — resume a completed response (SSR) or show a static example. |
| `simulate` | `string\|null` | `null` | Stream this text token by token from a local timer, with no `url` — a live-looking demo or a typewriter effect. |
| `simulateSpeed` | `int\|null` | `55` | Milliseconds per token in `simulate` mode. |
| `scope` | `string\|null` | `null` | Scoped personalization key. |

The Alpine scope exposes `text`, `status` (`idle` / `streaming` / `done` / `aborted`
/ `failed`), the derived `isStreaming` / `isDone` / `isAborted` / `isFailed` /
`isTerminal`, and the methods `start()`, `stop()`, and `restart()` — bind them from the
default slot for controls and custom state UIs. The `error` slot overrides the default
failure message.

## Accessibility

- **One live region.** A single visually-hidden `role="status" aria-live="polite"`
  region announces *generating* once, then the *result* (or *ready*) once. The visible
  output is deliberately **not** a live region, so a screen reader is never re-read on
  every token — the single most important decision this primitive makes.
- **Reduced motion.** Under `prefers-reduced-motion: reduce`, tokens are buffered and
  the full response is revealed at once — the incremental build is suppressed. The
  streaming caret only pulses when motion is allowed.
- **Defined terminal states.** Abort and connection loss resolve to `aborted` /
  `failed`, and the failure surfaces in a `role="alert"` region — never a silent hang.

## Keyboard Interaction

The output itself has no interaction model — it is streamed text. Any controls you add
in the default slot (Stop, Regenerate) are ordinary buttons: `Tab` to reach them,
`Enter` / `Space` to activate. There is nothing bespoke to learn.

## Design Tokens

| Token | Used for |
| --- | --- |
| `--text-wk-md` / `--color-wk-text` / `--font-wk-sans` | Streamed output type |
| `--color-wk-text-muted` | Streaming caret |
| `--color-wk-danger-text` | Failure message |
| `--gap-wk-xs` | Vertical rhythm between output, caret, and controls |

## Further Reading

- [MDN: Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
- [MDN: `EventSource`](https://developer.mozilla.org/en-US/docs/Web/API/EventSource)
- [MDN: `prefers-reduced-motion`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion)
