Migrating

Moving from react-markdown or Streamdown: what maps one to one, what moves to CSS, and what has no equivalent.

From react-markdown

flowdown reads Markdown with its own parser (it passes every example of CommonMark and of GitHub's extensions; see Conformance) and takes the props react-markdown made familiar, so most of a page carries over.

TSX
// Before
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import rehypeRaw from 'rehype-raw'
import rehypeSanitize from 'rehype-sanitize'

;<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw, rehypeSanitize]} components={components}>
  {text}
</ReactMarkdown>

// After
import { Markdown } from '@localix/flowdown'
import '@localix/flowdown/styles.css'

;<Markdown components={components}>{text}</Markdown>
react-markdownflowdown
childrenchildren
componentscomponents, the same map. Keep it stable. node is the package's own node, not a hast element.
remarkPlugins={[remarkGfm]}Built in.
rehypeRaw + rehypeSanitizeBuilt in, on by default (html).
skipHtmlhtml={false}
urlTransformurlTransform, the same function.
remark-math + rehype-katexmath={() => import('@localix/flowdown/math')}
rehype-highlighthighlight={highlight} from @localix/flowdown/highlight
className on a wrapperclassName
remarkPlugins, rehypePlugins of your ownNot accepted: there is no unified pipeline inside. Write the syntax as an extension, or keep react-markdown and add remarkFlowdown from @localix/flowdown/remark; see Extensions and remark.
allowElement, disallowedElements, unwrapDisallowedNot accepted. Override the element in components to drop or unwrap it.

What changes for the reader:

  • A streaming reply is parsed block by block, so a long reply costs about the same per token as a short one. Pass streaming while it arrives.
  • Quotes with [!NOTE]-style markers become callouts, and the forms ([!STEPS], [!TABS], …) render. Text that never used them looks as before.
  • Fenced code has a language bar and a copy button, and tables scroll sideways in their own box.
  • With raw HTML on, ids and names in a reply are prefixed user-content-, as on GitHub.

From Streamdown

Streamdown and flowdown solve the same problem, a reply that renders while it streams, in a similar way: both cut the reply into blocks, memoize them and complete the unfinished end. The differences are the dialect, the styling and how features are added.

TSX
// Before
import { Streamdown } from 'streamdown'

;<Streamdown isAnimating={isStreaming} caret="block">
  {text}
</Streamdown>

// After
import { Markdown } from '@localix/flowdown'
import '@localix/flowdown/styles.css'

;<Markdown streaming={isStreaming}>{text}</Markdown>
Streamdownflowdown
childrenchildren
isAnimatingstreaming
mode="static"streaming={false}, the default.
parseIncompleteMarkdownAlways on while streaming, off for the finished text.
caret="block" / "circle"caret, on by default while streaming. Its look is CSS: see Styling.
componentscomponents
classNameclassName
plugins.code (Shiki), shikiThemehighlight, and the --fd-code-* tokens for colours. See Highlighting.
plugins.mathmath. See Math.
plugins.mermaid, mermaid.configmermaid={() => import('mermaid')}, coloured from the page's tokens. See Diagrams.
controlsA copy button on each code block. Other buttons: override pre and wrap CodeBlock; see Components.
codeBlockMaxHeight, tableMaxHeightCSS on .fd-code pre and .fd-table.
linkSafetyOverride a in components, and urlTransform.
allowedTagsNo equivalent: the sanitizer's list is fixed. See Security.
remarkPlugins, rehypePluginsNot accepted; write an extension.
dirSet dir on an element around <Markdown>.
animatedNo equivalent. Animate in CSS if you want text to fade in.
BlockComponent, parseMarkdownIntoBlocksFnNo equivalent. createBlockSplitter from @localix/flowdown/remark is the splitter <Markdown> uses.
remend optionsNo options. The healing rules are in closePartialMarkdown; see Streaming.

Styling

Streamdown draws with Tailwind classes. flowdown ships its own stylesheet driven by --fd-* tokens, with no Tailwind needed. Remove the @source line Streamdown's setup added to your Tailwind CSS, import @localix/flowdown/styles.css, and set the tokens to your theme's colours; see Styling.

Streamdown's default hardening accepts links and images with any prefix and protocol. flowdown keeps the stricter check react-markdown has: relative URLs and http, https, mailto, irc, ircs, xmpp. A reply that linked to tel: or another app's scheme now gets an empty link; allow it with urlTransform, as shown in Security.

Prompt

Streamdown renders what the model writes. flowdown also gives the model the dialect to write: add answerFormatPrompt() to the system prompt to get steps, tabs and callouts. See Prompting.