PhoenixStreamdown (PhoenixStreamdown v1.0.0-beta.4)

Copy Markdown View Source

Streaming markdown renderer for Phoenix LiveView.

Renders LLM-streamed markdown with proper handling of incomplete syntax, block-level memoization, and minimal DOM updates.

Usage

use PhoenixStreamdown

<.markdown content={@response} streaming={@streaming?} />

Or without importing:

<PhoenixStreamdown.markdown content={@response} streaming={@streaming?} />

How it works

  1. PhoenixStreamdown.Remend — auto-closes incomplete markdown syntax (**bold**bold**, unclosed code fences, partial links)
  2. PhoenixStreamdown.Blocks — splits markdown into independent blocks
  3. MDEx — renders each block to HTML server-side (Rust-backed)
  4. LiveView — diffs only the active block, skips the rest

Completed blocks get phx-update="ignore" so LiveView skips them entirely. On a 56-block document, this means ~7x less server CPU and ~460x smaller diffs per token compared to re-rendering the full document each time.

Animations

Enable word-level streaming animations (like Vercel's Streamdown):

<.markdown content={@response} streaming animate="fadeIn" />

Available animations: fadeIn (default), blurIn, slideUp.

Include the CSS in your app:

@import "../../deps/phoenix_streamdown/priv/static/phoenix_streamdown.css";

Customization

Syntax highlighting theme

<.markdown content={@md} theme="catppuccin_mocha" />

Available themes: onedark (default), dracula, github_dark, github_light, catppuccin_mocha, nord, tokyonight_night, vscode_dark, and 100+ more.

CSS classes

<.markdown content={@md} class="prose" block_class="mb-4" />

Stable IDs

Each component auto-generates a unique id. For completed messages in a list, pass a stable ID to preserve frozen blocks across re-renders:

<.markdown content={msg.content} id={"msg-#{msg.id}"} />

The streaming component doesn't need an explicit id — it's ephemeral.

MDEx options

Options are deep-merged with defaults, so you only override what you need:

<.markdown content={@md} mdex_opts={[extension: [shortcodes: true]]} />

Why not just MDEx streaming?

MDEx has its own streaming: true mode. PhoenixStreamdown adds:

  • Block-level memoization — only the last block re-renders per token
  • Ready-made LiveView component — drop in, pass content and streaming flag
  • Remend — strips partial links/images instead of rendering broken HTML
  • Word-level animations — fade-in/blur-in each new word as it streams

Summary

Functions

Imports the markdown/1 component for use as <.markdown />.

Renders markdown content with streaming support.

Functions

__using__(opts)

(macro)

Imports the markdown/1 component for use as <.markdown />.

defmodule MyAppWeb.ChatLive do
  use MyAppWeb, :live_view
  use PhoenixStreamdown

  def render(assigns) do
    ~H"""
    <.markdown content={@response} streaming={@streaming?} />
    """
  end
end

markdown(assigns)

Renders markdown content with streaming support.

Attributes

  • content — the markdown string to render
  • streaming — whether content is still being streamed (enables incomplete syntax completion)
  • animate — animation type for streaming: "fadeIn", "blurIn", "slideUp", or nil to disable
  • class — CSS class for the wrapper <div>
  • block_class — CSS class for each block <div>
  • id — unique ID prefix (auto-generated; pass explicitly for stable IDs across re-renders)
  • theme — syntax highlighting theme name (default: "onedark")
  • mdex_opts — options passed to MDEx.to_html!/2 (merged with defaults)

Examples

<PhoenixStreamdown.markdown content="# Hello **world**" />

<PhoenixStreamdown.markdown
  content={@response}
  streaming
  animate="blurIn"
  class="prose"
  theme="github_dark"
/>

Attributes

  • content (:string) - Defaults to "".
  • streaming (:boolean) - Defaults to false.
  • animate (:string) - Defaults to nil.
  • class (:any) - Defaults to nil.
  • block_class (:any) - Defaults to nil.
  • id (:string)
  • theme (:string) - Defaults to "onedark".
  • mdex_opts (:list) - Defaults to [].