Markdown
markdownRenders read-only Markdown blocks, inline formatting, links, quotes, lists, and fenced code.
Usage
Basic usage
Pass in the Markdown source text as children, zero-dependency read-only rendering, and typesetting Prose semantics token.
Quick sort
Here is a concise implementation:
function quickSort(arr) {
if (arr.length <= 1) return arr;
const [pivot, ...rest] = arr;
return [
...quickSort(rest.filter((x) => x < pivot)),
pivot,
...quickSort(rest.filter((x) => x >= pivot)),
];
}Average complexity O(n log n), worst O(n²). Key points:
- Randomly select pivot to avoid the worst case scenario
- Inline
codeand external link render normally
Reference blocks are also supported, and the overall typesetting is Prose semantics token.
<Markdown>{`## Quick sort
Average complexity **O(n log n)**, key points:
- Randomly select *pivot* to avoid the worst case scenario
- Inline \`code\` and [external link](https://mdn.io) render normally
> Quote blocks are also supported. `}</Markdown>Form
Supports the GFM style table, which is rendered as a stroke container + shallow bottom table header + row zebra pattern.
Component comparison
| Component | Purpose | Editable |
|---|---|---|
| Markdown | Read-only rendering | No |
| MarkdownEditor | Rich text editing | Yes |
Inline code and bold render normally outside the table.
<Markdown>{`## Component comparison
| Component | Purpose | Editable |
| --- | --- | --- |
| Markdown | Read-only rendering | No |
| MarkdownEditor | Rich text editing | Yes |
`}</Markdown>Compact size
size="sm" is passed to the internal Prose, reducing the typography baseline to text-sm.
Quick sort
Here is a concise implementation:
function quickSort(arr) {
if (arr.length <= 1) return arr;
const [pivot, ...rest] = arr;
return [
...quickSort(rest.filter((x) => x < pivot)),
pivot,
...quickSort(rest.filter((x) => x >= pivot)),
];
}Average complexity O(n log n), worst O(n²). Key points:
- Randomly select pivot to avoid the worst case scenario
- Inline
codeand external link render normally
Reference blocks are also supported, and the overall typesetting is Prose semantics token.
<Markdown size="sm">{markdownSource}</Markdown>When to use
Use Markdown to render a source string as formatted, read-only content. Use [MarkdownEditor] when users must edit the source, Prose when content is already HTML or JSX, and Text for a single atomic passage. The exported parseBlocks helper supports custom rendering from the block-level AST. For long documents that need a table of contents or shareable #fragment links, turn on headingIds and build the entries with extractHeadings (see below).
Import
import { Markdown, parseBlocks, extractHeadings, slugifyHeading } from "@hulianui/ui"Props
| Name | Type | Default | Description |
|---|---|---|---|
| size | "sm" | "base" | "base" | Typography scale passed to the internal Prose component. |
| headingIds | boolean | string | false | Adds anchor ids to rendered headings (slug rules under "Heading anchors and tables of contents") so long pages can offer a table of contents and #fragment deep links. Off by default: ids share one global namespace, so generating them by default would add ids to every existing call site that may collide with ids already on the page. Pass a string to enable them and use it as an id prefix (headingIds="doc-" yields doc-props), which keeps this batch of ids in its own namespace. |
| className | string | - | Additional container class name. |
Slots
| Slot | Type | Description |
|---|---|---|
| children | string | Markdown source text (read-only rendering; editing with MarkdownEditor) |
Example
<div className="max-w-2xl">
<Markdown>{`## Quicksort
\`\`\`js
function quickSort(arr) { /* ... */ }
\`\`\`
Average complexity is **O(n log n)**. Inline \`code\` and [external links](https://mdn.io) render normally.
> Blockquotes inherit Prose semantic tokens.`}</Markdown>
</div>Heading anchors and tables of contents
Turn on headingIds to give every heading an id, then build the entries with extractHeadings from the same source string. Both sides share one slug rule, so the anchors cannot drift apart:
const md = "## Install\n\n### One-line setup\n\n## Rules";
const toc = extractHeadings(md).map((h) => ({ href: `#${h.id}`, title: h.plainText, level: h.level }));
<article className="[&_h2]:scroll-mt-20 [&_h3]:scroll-mt-20">
<Markdown headingIds>{md}</Markdown>
</article>
<Anchor items={toc.filter((t) => t.level === 2)} offsetTop={88} />When the host page carries ids of its own (page level sections, or an element rendered by one of the examples), pass a prefix instead: <Markdown headingIds="doc-"> together with extractHeadings(md, "doc-"). The two prefixes must match.
Each entry from extractHeadings is { level, text, plainText, id }. text keeps the original heading source including inline markers, while plainText has them stripped: use it for the table of contents label, because a plain string label would otherwise show backticks and asterisks verbatim.
Slug rules: strip inline markers (`code`, **bold**, *italic*, [text](link)), lowercase (ASCII only), collapse whitespace into hyphens, keep only Unicode letters, digits, -, and _ (so CJK headings survive verbatim), then collapse and trim hyphens. A heading that loses every character (empty or punctuation only) falls back to section. Repeated headings get -1 and -2 suffixes in document order. Use slugifyHeading(text) when you need the rule for a single heading.
Usage guidelines
- The dependency-free parser returns JSX and does not use
dangerouslySetInnerHTMLorinnerHTML, avoiding the stored-XSS sink described in [[dompurify-vhtml-markdown-sanitize]]. If raw HTML support is added later, sanitize it with DOMPurify before rendering; never send untrusted input directly toinnerHTML. - Markdown is read-only and exposes no editing callback. Use MarkdownEditor for bidirectional editing.
- Extract the table of contents from the same source string you render. A page that renders the body without its leading header but extracts from the full original ends up with a top-level entry that does not exist on the page, and clicking it goes nowhere.
- The component reserves no
scroll-mt-*for you. When a sticky header covers the landing position, add descendant classes such as[&_h2]:scroll-mt-20on an outer container, and keep that value in step with theoffsetTopofAnchor, otherwise the highlight always lags by one entry. - When the real scroll container is not the window (an inner
<main>withoverflow-y-auto, for example), passgetContainertoAnchorso clicks actually scroll.
Related
Anchor · Text · Heading · Prose · AuroraText · AnimatedShinyText · AnimatedGradientText
Playground
Quick sort
Here is a concise implementation:
function quickSort(arr) {
if (arr.length <= 1) return arr;
const [pivot, ...rest] = arr;
return [
...quickSort(rest.filter((x) => x < pivot)),
pivot,
...quickSort(rest.filter((x) => x >= pivot)),
];
}Average complexity O(n log n), worst O(n²). Key points:
- Randomly select pivot to avoid the worst case scenario
- Inline
codeand external link render normally
Reference blocks are also supported, and the overall typesetting is Prose semantics token.
<Markdown size="base">{`\n## Title\n\nText **bold** and \`code\`\n`}</Markdown>