Comark vs markdown-to-jsx
TL;DR: markdown-to-jsx is a zero-dependency Markdown compiler. It renders JSX tags in the content as your own components, and it outputs React, React Native, Solid, Vue, HTML, and Markdown. Comark uses a component syntax that isn't JSX and completes unterminated syntax while a response streams. It renders to Vue, React, Svelte, Angular, HTML, and the terminal.
At a glance
| Comark | markdown-to-jsx | |
|---|---|---|
| Components in the content | ::alert{type="info"} blocks and :badge[New] inline | PascalCase JSX tags, for example <DatePicker timezone="UTC+5" /> |
| Props | Inline attributes, typed :prop values, YAML props block | JSX props; arrays, objects, and booleans are parsed |
| Named slots | Yes, #footer maps to a slot or prop | Nested content arrives as children |
| Incomplete Markdown | autoClose completes it on every parse (on by default) | optimizeForStreaming hides it until it closes (off by default) |
| Outputs | Vue, React, Svelte, Angular, HTML, ANSI, Markdown | React, React Native, Solid, Vue, HTML, Markdown |
| Dependencies | markdown-exit, js-yaml, htmlparser2, entities | None |
| Raw HTML default | Parsed into nodes; add the security plugin with blockedTags for untrusted input | tagfilter escapes dangerous tags; raw HTML is always sanitized |
| Extensions | Comark plugins and markdown-it plugins | overrides, renderRule, createElement; no plugin system |
| Parsed output | Serializable MarkdownDocument | AST from parser(), rendered with astToJSX() or astToHTML() |
Two ways to write a component
Both libraries let the content place your components. The difference is the syntax that authors and models write.
markdown-to-jsx reads JSX. You register the component in overrides, and the parser converts JSON-like prop values for you:
import Markdown from 'markdown-to-jsx/react'
import Table from './Table'
const content = `<Table columns={['Name', 'Age']} data={[['Alice', 30], ['Bob', 25]]} />`
export function Page() {
return <Markdown options={{ overrides: { Table } }}>{content}</Markdown>
}Function props such as onClick={() => ...} stay strings. The evalUnserializableExpressions option evaluates them, and its documentation marks it as dangerous for user input.
Comark reads its own component syntax. Props go in {} or in a YAML block, content goes in slots, and components nest with extra colons:
::data-table
---
columns:
- Name
- Age
rows:
- [Alice, 30]
- [Bob, 25]
---
Team members.
#footer
Updated weekly.
::import { Markdown } from '@comark/react'
import DataTable from './DataTable'
export function Page({ content }: { content: string }) {
return <Markdown components={{ DataTable }}>{content}</Markdown>
}In React, the default slot arrives as children and #footer arrives as the slotFooter prop. See named slots.
JSX is familiar to React developers. Comark syntax is closer to Markdown, so writers and CMS editors can use it without knowing JSX. Neither syntax runs code from the content.
Streaming
Both libraries have a strategy for half-written Markdown, but the strategies differ.
markdown-to-jsx optimizeForStreaming holds back incomplete structures until they close. Unclosed bold, links, inline code, and HTML tags render nothing until the closing delimiter arrives. Fenced code blocks render as they arrive. A table waits for its first data row.
Comark autoClose completes the syntax instead. **bold renders as bold at once, and an open ::alert renders as a component while its content streams:
import { Markdown } from '@comark/react'
<Markdown streaming={isStreaming} caret>{text}</Markdown>While streaming is true, the caret prop adds a blinking cursor after the last text node. Try it in the streaming playground.
Security defaults
markdown-to-jsx is stricter out of the box. Its tagfilter option is on by default and escapes script, iframe, style, and similar tags. Its raw HTML sanitization is always on. It strips on* handlers, srcdoc, and javascript:, vbscript:, and non-image data: URLs.
Comark is open by default. Markdown link syntax with an unsafe URL doesn't become a link, and the framework renderers never forward innerHTML props. Raw HTML, its attributes, and its <script> tags are kept, though. For model output or user content, add the security plugin and list the tags to remove in blockedTags:
import security from '@comark/react/plugins/security'
const plugins = [security({ blockedTags: ['script', 'iframe', 'object', 'embed', 'form', 'style'] })]What markdown-to-jsx does well
- Zero dependencies: Comark's parser bundle is larger, because it includes markdown-exit, a YAML parser, an HTML parser, and entity tables.
- Output targets: React Native and Solid are outputs that Comark doesn't have.
- React Server Components: the
Markdowncomponent detects RSC and needs no'use client'directive. Comark'sMarkdownDocumentalso renders in a Server Component. - Secure defaults: the tag filter and raw HTML sanitization need no setup.
If you render trusted Markdown with JSX components in React, React Native, or Solid, markdown-to-jsx is a good fit. Choose Comark when you need components with named slots, streaming that completes syntax, or Svelte, Angular, or terminal output.
FAQ
<Table ...> as an HTML tag: the name becomes table and {...} values aren't parsed. Rewrite these tags as ::table{prop="value"} with typed :prop values or a YAML props block. See component syntax.parseMarkdown() returns a JSON-safe MarkdownDocument. Store it, cache it, or send it, then render it with <MarkdownDocument>. See the document model.Comark vs remark
remark and unified transform Markdown through mdast and hast syntax trees with a large plugin ecosystem. Comark parses into a compact document with component syntax, streaming, and multi-framework renderers.
Comark vs svelte-markdown
Compare Comark with svelte-markdown and its Svelte 5 successor @humanspeak/svelte-markdown. See how each handles custom renderers, components in content, LLM streaming, and SvelteKit SSR.