Renders Markdown as styled typography components with syntax-highlighted code blocks.
The Joke Tax Chronicles
Once upon a time, in a far-off land, there was a very lazy king who spent all day lounging on his throne.
The King's Plan
The king thought long and hard, and finally came up with a brilliant plan: he would tax the jokes in the kingdom.
"After all," he said, "everyone enjoys a good joke, so it's only fair that they should pay for the privilege."
The Joke Tax
The king's subjects were not amused. The taxes were:
- 1st level of puns: 5 gold coins
- 2nd level of jokes: 10 gold coins
- 3rd level of one-liners: 20 gold coins
Results
| King's Treasury | People's Happiness |
|---|---|
| Empty | Overflowing |
| Modest | Satisfied |
| Full | Ecstatic |
Use inline code for short snippets.
const taxJoke = (level: number) => level * 5import { MDParser } from "@/components/ui/md-parser"
const MARKDOWN = `# The Joke Tax ChroniclesInstallation#
pnpm dlx shadcn@latest add https://ui.tyap.me/r/styles/base/md-parser.jsonnpx shadcn@latest add https://ui.tyap.me/r/styles/base/md-parser.jsonyarn dlx shadcn@latest add https://ui.tyap.me/r/styles/base/md-parser.jsonbunx --bun shadcn@latest add https://ui.tyap.me/r/styles/base/md-parser.json
Install the required dependencies.
pnpm add unified remark-parse remark-rehype remark-gfm rehype-pretty-code hast-util-to-jsx-runtime hast-util-to-string unist-util-visitnpm install unified remark-parse remark-rehype remark-gfm rehype-pretty-code hast-util-to-jsx-runtime hast-util-to-string unist-util-visityarn add unified remark-parse remark-rehype remark-gfm rehype-pretty-code hast-util-to-jsx-runtime hast-util-to-string unist-util-visitbun add unified remark-parse remark-rehype remark-gfm rehype-pretty-code hast-util-to-jsx-runtime hast-util-to-string unist-util-visit
Copy and paste the following code into your project.
import * as React from "react"
import { cn } from "@/lib/utils"
export function TypographyH1({
className,
children,
...props
}: React.HTMLAttributes<HTMLHeadingElement>) {
return (
<h1
className={cn(
"scroll-m-20 text-4xl font-extrabold tracking-tight text-balance",
className
)}
{...props}
>
{children}
</h1>
)
}
export function TypographyH2({
className,
children,
...props
}: React.HTMLAttributes<HTMLHeadingElement>) {
return (
<h2
className={cn(
"mt-10 scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0",
className
)}
{...props}
>
{children}
</h2>
)
}
export function TypographyH3({
className,
children,
...props
}: React.HTMLAttributes<HTMLHeadingElement>) {
return (
<h3
className={cn(
"mt-8 scroll-m-20 text-2xl font-semibold tracking-tight",
className
)}
{...props}
>
{children}
</h3>
)
}
export function TypographyH4({
className,
children,
...props
}: React.HTMLAttributes<HTMLHeadingElement>) {
return (
<h4
className={cn(
"mt-8 scroll-m-20 text-xl font-semibold tracking-tight",
className
)}
{...props}
>
{children}
</h4>
)
}
export function TypographyP({
className,
children,
...props
}: React.HTMLAttributes<HTMLParagraphElement>) {
return (
<p
className={cn("leading-7 [&:not(:first-child)]:mt-6", className)}
{...props}
>
{children}
</p>
)
}
export function TypographyLead({
className,
children,
...props
}: React.HTMLAttributes<HTMLParagraphElement>) {
return (
<p className={cn("text-xl text-muted-foreground", className)} {...props}>
{children}
</p>
)
}
export function TypographyLarge({
className,
children,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div className={cn("text-lg font-semibold", className)} {...props}>
{children}
</div>
)
}
export function TypographySmall({
className,
children,
...props
}: React.HTMLAttributes<HTMLElement>) {
return (
<small
className={cn("text-sm leading-none font-medium", className)}
{...props}
>
{children}
</small>
)
}
export function TypographyMuted({
className,
children,
...props
}: React.HTMLAttributes<HTMLParagraphElement>) {
return (
<p className={cn("text-sm text-muted-foreground", className)} {...props}>
{children}
</p>
)
}
export function TypographyBlockquote({
className,
children,
...props
}: React.HTMLAttributes<HTMLQuoteElement>) {
return (
<blockquote
className={cn("mt-6 border-l-2 pl-6 italic", className)}
{...props}
>
{children}
</blockquote>
)
}
export function TypographyUL({
className,
children,
...props
}: React.HTMLAttributes<HTMLUListElement>) {
return (
<ul className={cn("my-6 ml-6 list-disc [&>li]:mt-2", className)} {...props}>
{children}
</ul>
)
}
export function TypographyOL({
className,
children,
...props
}: React.HTMLAttributes<HTMLOListElement>) {
return (
<ol
className={cn("my-6 ml-6 list-decimal [&>li]:mt-2", className)}
{...props}
>
{children}
</ol>
)
}
export function TypographyInlineCode({
className,
children,
...props
}: React.HTMLAttributes<HTMLElement>) {
return (
<code
className={cn(
"relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold",
className
)}
{...props}
>
{children}
</code>
)
}
export function TypographyA({
className,
children,
href,
...props
}: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
return (
<a
href={href}
className={cn(
"font-medium text-primary underline underline-offset-4",
className
)}
{...props}
>
{children}
</a>
)
}
export function TypographyTable({
className,
children,
...props
}: React.HTMLAttributes<HTMLTableElement>) {
return (
<div className="my-6 w-full overflow-x-auto rounded-2xl border bg-background/70 shadow-xs ring-1 ring-border/30">
<table
className={cn(
"w-full min-w-max border-separate border-spacing-0 text-sm",
className
)}
{...props}
>
{children}
</table>
</div>
)
}
export function TypographyTR({
className,
children,
...props
}: React.HTMLAttributes<HTMLTableRowElement>) {
return (
<tr
className={cn(
"m-0 border-0 p-0 transition-colors even:bg-muted/30 hover:bg-muted/45 [&:last-child>td]:border-b-0",
className
)}
{...props}
>
{children}
</tr>
)
}
export function TypographyTH({
className,
children,
...props
}: React.ThHTMLAttributes<HTMLTableCellElement>) {
return (
<th
className={cn(
"border-r border-b bg-muted/70 px-4 py-3 text-left font-semibold text-muted-foreground first:rounded-tl-[calc(var(--radius-2xl)-1px)] last:rounded-tr-[calc(var(--radius-2xl)-1px)] last:border-r-0 [&[align=center]]:text-center [&[align=right]]:text-right",
className
)}
{...props}
>
{children}
</th>
)
}
export function TypographyTD({
className,
children,
...props
}: React.TdHTMLAttributes<HTMLTableCellElement>) {
return (
<td
className={cn(
"border-r border-b border-border/60 px-4 py-3 text-left align-top text-foreground/90 last:border-r-0 [&[align=center]]:text-center [&[align=right]]:text-right",
className
)}
{...props}
>
{children}
</td>
)
}
export function TypographyHR({
className,
...props
}: React.HTMLAttributes<HTMLHRElement>) {
return <hr className={cn("my-4 md:my-8", className)} {...props} />
}
export function TypographyStrong({
className,
children,
...props
}: React.HTMLAttributes<HTMLElement>) {
return (
<strong className={cn("font-semibold", className)} {...props}>
{children}
</strong>
)
}
export function TypographyEM({
className,
children,
...props
}: React.HTMLAttributes<HTMLElement>) {
return (
<em className={cn("italic", className)} {...props}>
{children}
</em>
)
}
import * as React from "react"
import { Fragment, jsx, jsxs } from "react/jsx-runtime"
import {
transformerNotationDiff,
transformerNotationErrorLevel,
transformerNotationFocus,
transformerNotationHighlight,
transformerNotationWordHighlight,
} from "@shikijs/transformers"
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
import rehypePrettyCode from "rehype-pretty-code"
import remarkGfm from "remark-gfm"
import remarkParse from "remark-parse"
import remarkRehype from "remark-rehype"
import type { ShikiTransformer } from "shiki"
import { unified } from "unified"
import { visit } from "unist-util-visit"
import { cn } from "@/lib/utils"
import { MDCodeBlock } from "@/components/ui/md-parser-code-block"
// Mirror the Shiki notation transformers used by the standalone CodeBlock so
// ```diff / [!code highlight] / [!code focus] annotations render identically.
const CODE_TRANSFORMERS: ShikiTransformer[] = [
transformerNotationDiff({ matchAlgorithm: "v3" }),
transformerNotationHighlight({ matchAlgorithm: "v3" }),
transformerNotationWordHighlight({ matchAlgorithm: "v3" }),
transformerNotationFocus({ matchAlgorithm: "v3" }),
transformerNotationErrorLevel({ matchAlgorithm: "v3" }),
]
import {
TypographyA,
TypographyBlockquote,
TypographyEM,
TypographyH1,
TypographyH2,
TypographyH3,
TypographyH4,
TypographyHR,
TypographyInlineCode,
TypographyOL,
TypographyP,
TypographyStrong,
TypographyTable,
TypographyTD,
TypographyTH,
TypographyTR,
TypographyUL,
} from "@/components/ui/typography"
interface MDParserProps {
children: string
className?: string
}
type RuntimeOptions = Parameters<typeof toJsxRuntime>[1]
type MarkdownElementProps = {
children?: React.ReactNode
href?: string
"data-language"?: string
"data-raw"?: string
"data-rehype-pretty-code-figure"?: unknown
[key: string]: unknown
}
type MarkdownElementComponent = (props: MarkdownElementProps) => React.ReactNode
type HastElement = {
type: string
tagName?: string
children?: unknown[]
properties?: Record<string, unknown>
}
const isHastElement = (node: unknown): node is HastElement =>
typeof node === "object" &&
node !== null &&
"type" in node &&
(node as { type: unknown }).type === "element"
const toComponentProps = <TProps extends object>(
props: Omit<MarkdownElementProps, "children">
) => props as TProps
function addLanguageToFigure() {
return (tree: Parameters<typeof visit>[0]) => {
visit(tree, "element", (node) => {
const element = node as HastElement
if (
element.tagName === "figure" &&
element.properties?.["data-rehype-pretty-code-figure"] !== undefined
) {
const pre = element.children?.find(
(child): child is HastElement =>
isHastElement(child) && child.tagName === "pre"
)
const code = pre?.children?.find(
(child): child is HastElement =>
isHastElement(child) && child.tagName === "code"
)
if (code?.properties?.["data-language"]) {
element.properties["data-language"] = code.properties["data-language"]
}
}
})
}
}
async function MDParser({ children, className }: MDParserProps) {
const processor = unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypePrettyCode, {
theme: { dark: "github-dark", light: "github-light" },
keepBackground: false,
transformers: CODE_TRANSFORMERS,
})
.use(addLanguageToFigure)
const mdast = processor.parse(children)
const hast = await processor.run(mdast)
const content = toJsxRuntime(hast, {
development: false,
Fragment,
jsx: jsx as RuntimeOptions["jsx"],
jsxs: jsxs as RuntimeOptions["jsxs"],
components: {
figure: ({
children: c,
"data-rehype-pretty-code-figure": isFigure,
"data-raw": raw,
"data-language": lang,
...p
}: MarkdownElementProps) => {
if (isFigure !== undefined) {
return (
<MDCodeBlock raw={raw ?? ""} language={lang}>
{c}
</MDCodeBlock>
)
}
return (
<figure {...toComponentProps<React.ComponentProps<"figure">>(p)}>
{c}
</figure>
)
},
h1: ({ children: c, ...p }: MarkdownElementProps) => (
<TypographyH1
{...toComponentProps<React.ComponentProps<typeof TypographyH1>>(p)}
>
{c}
</TypographyH1>
),
h2: ({ children: c, ...p }: MarkdownElementProps) => (
<TypographyH2
{...toComponentProps<React.ComponentProps<typeof TypographyH2>>(p)}
>
{c}
</TypographyH2>
),
h3: ({ children: c, ...p }: MarkdownElementProps) => (
<TypographyH3
{...toComponentProps<React.ComponentProps<typeof TypographyH3>>(p)}
>
{c}
</TypographyH3>
),
h4: ({ children: c, ...p }: MarkdownElementProps) => (
<TypographyH4
{...toComponentProps<React.ComponentProps<typeof TypographyH4>>(p)}
>
{c}
</TypographyH4>
),
p: ({ children: c, ...p }: MarkdownElementProps) => (
<TypographyP
{...toComponentProps<React.ComponentProps<typeof TypographyP>>(p)}
>
{c}
</TypographyP>
),
blockquote: ({ children: c, ...p }: MarkdownElementProps) => (
<TypographyBlockquote
{...toComponentProps<
React.ComponentProps<typeof TypographyBlockquote>
>(p)}
>
{c}
</TypographyBlockquote>
),
ul: ({ children: c, ...p }: MarkdownElementProps) => (
<TypographyUL
{...toComponentProps<React.ComponentProps<typeof TypographyUL>>(p)}
>
{c}
</TypographyUL>
),
ol: ({ children: c, ...p }: MarkdownElementProps) => (
<TypographyOL
{...toComponentProps<React.ComponentProps<typeof TypographyOL>>(p)}
>
{c}
</TypographyOL>
),
a: ({ href, children: c, ...p }: MarkdownElementProps) => (
<TypographyA
href={href}
{...toComponentProps<React.ComponentProps<typeof TypographyA>>(p)}
>
{c}
</TypographyA>
),
table: ({ children: c, ...p }: MarkdownElementProps) => (
<TypographyTable
{...toComponentProps<React.ComponentProps<typeof TypographyTable>>(p)}
>
{c}
</TypographyTable>
),
tr: ({ children: c, ...p }: MarkdownElementProps) => (
<TypographyTR
{...toComponentProps<React.ComponentProps<typeof TypographyTR>>(p)}
>
{c}
</TypographyTR>
),
th: ({ children: c, ...p }: MarkdownElementProps) => (
<TypographyTH
{...toComponentProps<React.ComponentProps<typeof TypographyTH>>(p)}
>
{c}
</TypographyTH>
),
td: ({ children: c, ...p }: MarkdownElementProps) => (
<TypographyTD
{...toComponentProps<React.ComponentProps<typeof TypographyTD>>(p)}
>
{c}
</TypographyTD>
),
hr: (p: MarkdownElementProps) => (
<TypographyHR
{...toComponentProps<React.ComponentProps<typeof TypographyHR>>(p)}
/>
),
strong: ({ children: c, ...p }: MarkdownElementProps) => (
<TypographyStrong
{...toComponentProps<React.ComponentProps<typeof TypographyStrong>>(
p
)}
>
{c}
</TypographyStrong>
),
em: ({ children: c, ...p }: MarkdownElementProps) => (
<TypographyEM
{...toComponentProps<React.ComponentProps<typeof TypographyEM>>(p)}
>
{c}
</TypographyEM>
),
code: ({ children: c, ...p }: MarkdownElementProps) => {
if (!p["data-language"]) {
return <TypographyInlineCode>{c}</TypographyInlineCode>
}
return (
<code {...toComponentProps<React.ComponentProps<"code">>(p)}>
{c}
</code>
)
},
} satisfies Record<
string,
MarkdownElementComponent
> as RuntimeOptions["components"],
})
return <div className={cn("", className)}>{content}</div>
}
export { MDParser }
Update the import paths to match your project setup.
Usage#
import { MDParser } from "@/components/ui/md-parser"<MDParser>{markdownString}</MDParser>Components#
MD Parser composes two separate component families that can also be used standalone.
Typography#
The typography component exports semantic HTML element wrappers with consistent typographic styles. These are used internally by MDParser to render all Markdown text elements.
import {
TypographyH1,
TypographyH2,
TypographyH3,
TypographyH4,
TypographyP,
TypographyLead,
TypographyLarge,
TypographySmall,
TypographyMuted,
TypographyBlockquote,
TypographyUL,
TypographyOL,
TypographyInlineCode,
TypographyA,
TypographyTable,
TypographyTR,
TypographyTH,
TypographyTD,
TypographyHR,
TypographyStrong,
TypographyEM,
} from "@/components/ui/typography"All Typography components accept a className prop for customization on top of the base styles.
<TypographyH1>The Joke Tax Chronicles</TypographyH1>
<TypographyLead>
A story about a king who taxed jokes.
</TypographyLead>
<TypographyP>
Use <TypographyInlineCode>inline code</TypographyInlineCode> within prose.
</TypographyP>CodeBlock (MD Parser Code Block)#
The md-parser-code-block component wraps syntax-highlighted <pre> output from rehype-pretty-code inside the design-system CodeBlock shell — complete with a language label and an animated copy button.
import { MDCodeBlock } from "@/components/ui/md-parser-code-block"<MDCodeBlock raw={codeString} language="tsx">
{/* pre element from rehype-pretty-code */}
</MDCodeBlock>| Prop | Type | Default | Description |
|---|---|---|---|
raw | string | — | Plain-text code used by the copy button |
language | string | — | Language label shown in the header |
children | React.ReactNode | — | Pre-highlighted <pre> element |
API Reference#
MDParser#
| Prop | Type | Default | Description |
|---|---|---|---|
children | string | — | Markdown string to render |
className | string | "" | Additional class for the wrapper |