Markdown that draws while the model is still writing

flowdown renders an AI reply in React as it streams: GitHub Markdown plus steps, tabs, figures, diagrams and math. Its own parser, no dependencies, 32 KB.

npm install @localix-ai/flowdown

React 18 and 19 · MIT · Docs · also outside React

Streaming a reply

  1. Install
    One package, no dependencies.
    sh
    npm install @localix-ai/flowdown
    
  2. Render
    Pass streaming while the text is still arriving.
    reply.tsxtsx
    import { Markdown } from '@localix-ai/flowdown'
    import '@localix-ai/flowdown/styles.css'
    
    export function Reply({ text, done }) {
      return <Markdown streaming={!done}>{text}</Markdown>
    }
    
  3. Teach the model
    Add answerFormatPrompt() to the system prompt, and it writes these forms.
  • 0.14 sto stream 100k characters
  • 32 KBgzipped
  • 0dependencies

The same reply, outside React:

ts
import { renderToHtml } from '@localix-ai/flowdown/static'

const html = renderToHtml(reply)

Streaming 100k charactersTime
flowdown0.14 s
markdown-to-jsx2.4 s
Streamdown6.5 s

Read more

1,736 characters

Built for a reply, not a document

Six things it does

Streaming

Nothing half-written reaches the reader

A reply arrives mid-word. Unfinished **bold, a half-typed link and an open code fence are repaired as they come, so raw syntax never shows and nothing flickers when it closes.

arrivesis drawn asThe **answer isThe answer is  (bold)see [the guisee the gui```ts⏎const a =a code block, still open| a | b |⏎| -a table, its first row$$ \frac{1held until it can be setonly the block being written is parsed again

Dialect

Forms a model can write and a reader can scan

Steps, tabs, headline figures, the case for and against, callouts, folded sections, sources and file trees, on top of GitHub Markdown. Each is a quote line and a list: where it is not rendered, it still reads.

Rolling back a deploy

  1. Find the last good release
    git log --oneline --first-parent main
  2. Deploy it
    bun run deploy --ref v1.4.2
  3. Check the health page
    It turns green within a minute.
  • 40 sto roll back
  • 0requests dropped
deploy
releases
v1.4.2the last good one
v1.5.0the bad one
current -> releases/v1.4.2
Why not revert the commit?

A revert builds and ships a new release, which takes minutes. A rollback points current at a release that is already built.

Simplicity is prerequisite for reliability. — Edsger W. Dijkstra

  • rolled back
  • write the incident note

Read more

902 characters

Speed

A long reply costs what a short one does

A 100k-character reply streams in 0.14 s of main-thread time; the next fastest of six React renderers takes 2.4 s. No update takes more than 5 ms, so a stream never drops a frame.

streaming, mschat 5kcode 50kanswer 100kflowdown1249138markdown-to-jsx15632,393markstream411,0765,714Streamdown515,9266,529marked-react37807,709react-markdown2133,808stopped32 KB gzipped, one package, no dependencies

Targets

The same reply outside React

As HTML text on a server, in a page with no framework, as a <flow-down> element, or as the rows of a terminal: this one is @localix-ai/flowdown/tui, live. One parser draws them all, and none of these entries imports React.

Move the API to Bun
▌ Warning
▌ Back up package-lock.json first.
1 Install
│ Run bun install.
│
2 Swap the scripts
Replace node with bun in package.json.
src/server.ts
import { serve } from 'bun'
serve({ port: 3000, fetch: () => new Response('ok') })
- "start": "node dist/server.js"
+ "start": "bun src/server.ts"
│ Check │ Before │ After │
├────────────┼────────┼───────┤
│ Cold start │ 410 ms │ 95 ms │
│ p95 │ 38 ms │ 21 ms │
✓ lockfile backed up
☐ deploy to staging
Throughput is r = n / t, and over k samples
r̄ = 1/k ∑ᵢ₌₁ᵏ rᵢ
Further reading
[1] Bun runtime — what differs from Node
https://bun.sh/docs/runtime
765 characters

Diagrams and math

A diagram a reader can open

A mermaid fence is drawn once it is whole, in the page’s colours, and opens in a viewer that zooms and pans: try it. Formulas are set by KaTeX. Both load only for a reply that has one.

Between your message and the reply

The page draws while the model writes:

sequenceDiagram
  participant You
  participant App
  participant Model
  You->>App: Send a message
  App->>Model: The conversation
  loop Every token
    Model-->>App: The text so far
    App-->>You: Draw the last block again
  end
  Model-->>App: Done
Drawing diagram…

Hover the diagram for its toolbar: Enlarge opens a viewer that zooms and pans.

With $n$ characters arriving at $r$ a second, the reply takes

$$ t = \frac{n}{r} $$

and each update costs the last block, not the whole text.

577 characters

Safety

A reply cannot act on the page

A model’s text is untrusted. Raw HTML is parsed and sanitized, scripts and handlers are dropped, and a link keeps only a safe protocol. It needs no setup and no second library.

the reply<img src=x onerror="steal()">[click](javascript:alert(1))<script>fetch('/token')</script>the page<img src="x"/><p><a>click</a></p>(nothing)

Documentation

19 pages →

Releases

Full changelog →
  1. v0.5.2

    The package's repository is localixai/flowdown: the links of its page on npm and of its README

    1 change

  2. v0.5.1

    document.querySelector('flow-down') is typed as the element, so markdown and streaming are there without a cast once @localix-ai/flowdown/element is imported. - 72542f8: A long reply streams faster in Chrome, Edge and Node. Each token, the parts that remember a growing text first check that the new text continues the last one. That check was startsWith on a slice, which V8 walks a character at a time: seventy microseconds on a 35 KB reply, three times per token. It is === on two slices now, which V8 compares as memory, in one microsecond. Bun and Safari (JavaScriptCore) were already fast either way, which is why the benchmark, run on Bun, never saw it.

    5 changes

  3. v0.5.0

    <Markdown> draws what it drew. It now loads 31.7 KB gzipped, from 30.5: the renderer is shared by every target.

    3 changes

  4. v0.4.3

    A shorter README and package description

    1 change

  5. v0.4.2

    Two replies that stream at once no longer slow each other down. The scan for a fence left open remembered one text for the whole page, so each reply made the other's start over on every token: two 100k-character replies side by side cost twice what they cost one after the other. Each <Markdown> now has a scan of its own.

    1 change

Start in three steps

Free · MIT license

  1. Install

    npm install @localix-ai/flowdown. React is its only peer, and only for <Markdown>.

  2. Render

    Import @localix-ai/flowdown/styles.css once, then <Markdown streaming={!done}>{text}</Markdown>.

  3. Teach the model

    Add answerFormatPrompt() from @localix-ai/flowdown/prompt to the system prompt.