Comark vs react-markdown
TL;DR: react-markdown renders standard Markdown to React elements and lets you remap HTML tags to components. Comark does that too, and adds what react-markdown was never designed for: component syntax authors can write in the content itself, auto-close for token-by-token streaming, a serializable AST you can parse on the server, and renderers beyond React.
At a glance
| Comark | react-markdown | |
|---|---|---|
| Runtime parsing | Yes | Yes |
Remap h1, a, code… to components | Yes, components map | Yes, components prop |
| Custom components in the content | Yes, ::alert{type="info"} | No, element remapping only |
| Streaming incomplete Markdown | Auto-close renders every frame correctly | Broken formatting until syntax completes |
| Frameworks | Vue, React, Svelte, Angular, Nuxt, HTML, ANSI | React only |
| Parse/render split | Decoupled, parse on server and render on client | Coupled, parsing happens inside the component |
| Underlying parser | markdown-exit (markdown-it compatible) | remark (unified) |
| Plugins | Comark plugins + markdown-it plugins | remark/rehype plugins |
| Frontmatter | Parsed out of the box | Needs remark-frontmatter + custom handling |
Element mapping works the same way
If you use react-markdown only to restyle elements, the model transfers directly:
import Markdown from 'react-markdown'
<Markdown
components={{
a: (props) => <Link {...props} />,
code: (props) => <CodeBlock {...props} />,
}}
>
{markdown}
</Markdown>import { Markdown } from '@comark/react'
<Markdown
components={{
a: Link,
code: CodeBlock,
}}
>
{markdown}
</Markdown>What react-markdown can't express
react-markdown maps existing HTML elements to components. It has no syntax for components that aren't HTML elements: a callout, a tabbed code group, a video embed. The usual workaround is raw HTML plus rehype-raw, which reintroduces the verbosity and XSS surface Markdown was meant to avoid.
Comark makes components part of the syntax:
::alert{type="warning"}
This is **Markdown** inside a component, with full nesting support.
::
Status: :badge[Active]{color="green"}Writers, CMS editors, and LLMs can produce these components as plain text. No JSX, no HTML. See component syntax.
Streaming
react-markdown assumes a complete document. Feed it a stream mid-token and you get flashing asterisks, broken lists, and half-rendered code fences until the closing syntax arrives. This is the exact problem that led Vercel to build Streamdown.
Comark treats incomplete input as a first-class case. Auto-close completes unterminated syntax at every frame, and the streaming prop handles it end to end:
import { Markdown } from '@comark/react'
import { useChat } from '@ai-sdk/react'
export function Message({ text, isStreaming }: { text: string, isStreaming: boolean }) {
return <Markdown streaming={isStreaming}>{text}</Markdown>
}Decoupled parsing
react-markdown parses inside the React component, on every client that renders it. Comark separates the two: parse once (on the server, at build time, or in a worker) and render the compact AST anywhere:
import { parseMarkdown } from 'comark'
const document = await parseMarkdown(markdown) // cache it, store it, send itimport { MarkdownDocument } from '@comark/react'
<MarkdownDocument value={document} components={{ Alert }} />The document contains plain arrays like ['h1', { id: 'hello' }, 'Hello'], which are cheap to serialize and cache. See the document model.
What react-markdown does well
- The unified ecosystem: hundreds of remark/rehype plugins. Comark's answer is its own plugin API plus compatibility with markdown-it plugins; check that your must-have plugins have an equivalent before switching.
- Maturity: react-markdown has been battle-tested for years across a huge install base.
If you render trusted, complete, standard Markdown in a React-only app and rely on specific remark plugins, react-markdown remains a solid choice.
FAQ
components mapping concept carries over. Swap the component first, then adopt ::component syntax where you need it.