DEV Community

Cover image for How we built streaming-safe Markdown for AI chat in React
ozoduwdev for anythic

Posted on

How we built streaming-safe Markdown for AI chat in React

If you've built an AI chat UI, you've seen this: the model streams half a code block, and suddenly your whole reply turns into a giant monospace mess. A ** opens bold and never closes. A link renders as [text](htt. Then the next token arrives and everything jumps back.

We hit this so often that we ended up building a component library around it: anythic/ui, open-source React components for AI apps.

The problem: Markdown is not designed to be incomplete

Markdown parsers assume they get the whole document. During streaming, they only get a prefix:

Here is the fix:

```ts
function add(a, b) {
  return a +
Enter fullscreen mode Exit fullscreen mode

The code fence is open, so everything after it is treated as code, including text that hasn't arrived yet. With every token, the layout flips back and forth.

The fix: repair the prefix before parsing

Instead of rendering the raw prefix, we "complete" it first: close the open code fence, close unbalanced **, _ and backticks, and hold back a link until its URL is complete. The parser always sees valid Markdown, so the reply grows smoothly instead of flickering.

In anythic/ui this happens inside the Markdown component. You just pass the text as it streams:

import { Markdown } from "@anythic/ui";

<Markdown>{streamingText}</Markdown>
Enter fullscreen mode Exit fullscreen mode

The rest of an AI interface

Once Markdown worked, we kept going with the other pieces every agent UI needs:

  • ReasoningBlock: collapsible "thinking" that shows how long the model thought
  • ToolCall and ToolApproval: show what the agent is doing, and ask before dangerous actions
  • Plan: a live checklist of the agent's steps
  • CodeDiff: file changes with an in-house Myers diff
  • Terminal: command output with an ANSI color parser and progress bars
  • Citation and Sources: numbered references that match [1] in the text
  • PromptInput, Conversation, Message: the chat shell itself

Everything is model-agnostic: components take plain props, so they work with the Vercel AI SDK, OpenAI, Anthropic, Gemini or your own backend. Styling uses Tailwind v4 with shadcn-compatible tokens, so it drops into existing shadcn projects and supports dark mode out of the box.

Try it

npm i @anythic/ui
Enter fullscreen mode Exit fullscreen mode

The repo includes screenshots of every component and a demo app you can run locally with your own Gemini key:
👉 https://github.com/anythic/anythic-ui

It's MIT licensed. We'd love your feedback: what's the most annoying part of building AI UIs for you, and which component should we build next?**
 **

Top comments (0)