Comark vs markdown-to-jsx

markdown-to-jsx renders JSX tags in Markdown with zero dependencies and many output targets. Comark adds a component syntax with YAML props and named slots, streaming auto-close, and Svelte, Angular, and terminal renderers.

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

Comarkmarkdown-to-jsx
Components in the content::alert{type="info"} blocks and :badge[New] inlinePascalCase JSX tags, for example <DatePicker timezone="UTC+5" />
PropsInline attributes, typed :prop values, YAML props blockJSX props; arrays, objects, and booleans are parsed
Named slotsYes, #footer maps to a slot or propNested content arrives as children
Incomplete MarkdownautoClose completes it on every parse (on by default)optimizeForStreaming hides it until it closes (off by default)
OutputsVue, React, Svelte, Angular, HTML, ANSI, MarkdownReact, React Native, Solid, Vue, HTML, Markdown
Dependenciesmarkdown-exit, js-yaml, htmlparser2, entitiesNone
Raw HTML defaultParsed into nodes; add the security plugin with blockedTags for untrusted inputtagfilter escapes dangerous tags; raw HTML is always sanitized
ExtensionsComark plugins and markdown-it pluginsoverrides, renderRule, createElement; no plugin system
Parsed outputSerializable MarkdownDocumentAST 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:

markdown-to-jsx
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:

content.md
::data-table
---
columns:
  - Name
  - Age
rows:
  - [Alice, 30]
  - [Bob, 25]
---
Team members.

#footer
Updated weekly.
::
Page.tsx
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 Markdown component detects RSC and needs no 'use client' directive. Comark's MarkdownDocument also 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

© 2026 Vercel, Inc.