Prose
Styles the HTML a markdown renderer produces, so long-form content picks up the design system's type, spacing, and color.
Installation
Usage
import { Prose } from "@/components/ui/prose"Examples
Default
Headings, paragraphs, inline code, a code block, a blockquote, a list, and a table, all styled from the tokens with no per-element classes.
Example
Paste a raw meeting transcript and get back a clean recap. The renderer turns markdown into HTML; Prose gives it the type, spacing, and color from your design system.
Alice: We decided to launch the beta on Friday.
Bob: The mobile app will be postponed to next quarter.
Carol: Priya will finalize the release notes by Thursday.
Dave: Sam will email beta testers by Friday.Recap: the team agreed to launch the beta on Friday and to postpone the mobile app to next quarter. See the full thread for context.
Ship the smallest thing that proves the idea, then decide what is next.
Decisions
- Launch beta on Friday
- Mobile app postponed to next quarter
Action items
| Owner | Task | Due |
|---|---|---|
| Priya | Finalize release notes | Thu |
| Sam | Email beta testers | Fri |
Inline formatting
Bold, italic, strikethrough, inline code, links, an ordered list, and a rule.
Inline formatting covers bold, italic, struck-through, inline code, and links. Ordered lists number themselves:
- Draft the recap
- Confirm the owners
- Send by end of day
A rule breaks one section from the next without adding a heading.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | - | Merged onto the root. Pass max-w-none to drop the reading-measure cap, or override any element style. |
| children* | ReactNode | - | The rendered HTML to style. Typically the output of a markdown renderer. |
import { cn } from "@/registry/lib/utils"
/**
* Prose styles the HTML that a markdown renderer produces. Drop the rendered
* output inside and every element (headings, paragraphs, links, code, lists,
* blockquotes, tables) picks up the design system's tokens. It does not parse
* markdown; pair it with whatever renderer the app already uses.
*
* Spacing comes from `prose-flow` (vertical rhythm owned by the following
* element) and the block caps at the reading measure. Wide code blocks and
* tables scroll inside that measure rather than overflow the page. To opt out
* of the cap, pass `className="max-w-none"`.
*/
// Element styling. Grouped by element, each rule token-driven so a re-themed
// consumer inherits the change. Descendant selectors are the only way to reach
// HTML this component did not author, so the styling lives here, not on tags.
const proseStyles = [
// Headings: strong, balanced wrapping, tight leading. Real weights only.
"[&_h1]:text-2xl [&_h1]:font-semibold [&_h1]:leading-snug [&_h1]:text-foreground [&_h1]:text-balance",
"[&_h2]:text-xl [&_h2]:font-semibold [&_h2]:leading-snug [&_h2]:text-foreground [&_h2]:text-balance",
"[&_h3]:text-lg [&_h3]:font-semibold [&_h3]:leading-snug [&_h3]:text-foreground [&_h3]:text-balance",
"[&_h4]:text-base [&_h4]:font-semibold [&_h4]:leading-snug [&_h4]:text-foreground [&_h4]:text-balance",
"[&_h5]:text-sm [&_h5]:font-semibold [&_h5]:leading-snug [&_h5]:text-foreground [&_h5]:text-balance",
"[&_h6]:text-sm [&_h6]:font-semibold [&_h6]:leading-snug [&_h6]:text-muted-foreground [&_h6]:text-balance",
// Body copy: the kit's 14px body, pretty wrapping so no orphan words. Color
// comes from the muted-foreground root below (matching the type-body tier);
// headings, bold, and links step back up to foreground/accent explicitly.
"[&_p]:text-sm [&_p]:text-pretty",
// Inline emphasis. Bold and italic use loaded weights, never synthesized.
"[&_strong]:font-semibold [&_strong]:text-foreground [&_b]:font-semibold [&_b]:text-foreground",
"[&_em]:italic [&_i]:italic",
"[&_del]:text-muted-foreground [&_del]:line-through [&_s]:text-muted-foreground [&_s]:line-through",
// Links: the kit's inline-link rule. Accent, underlined at rest, plain on
// hover; the underline pulls its weight from the font metrics.
"[&_a]:text-accent [&_a]:underline [&_a]:decoration-from-font [&_a]:underline-offset-2 [&_a]:rounded-sm [&_a:hover]:no-underline",
"[&_a:focus-visible]:ring-2 [&_a:focus-visible]:ring-ring-accent [&_a:focus-visible]:outline-none",
// Inline code: mono on the kit's subtle chip (bg-neutral-500/10, the same
// idiom as a subtle Button; survives on a card in either theme where
// bg-muted would not). Size inherits the surrounding text. The pre reset
// below keeps this off code blocks.
"[&_:not(pre)>code]:font-mono [&_:not(pre)>code]:bg-neutral-500/10 [&_:not(pre)>code]:rounded-sm [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5",
// Code blocks: the Card tier (ringed surface, rounded-lg), matching the
// CodeBlock component. Wide lines scroll inside the block. The inner code
// resets to the code-block type (text-xs, relaxed leading).
"[&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:bg-card [&_pre]:p-4 [&_pre]:text-sm [&_pre]:shadow-ring-md",
"[&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:font-mono [&_pre_code]:font-normal [&_pre_code]:text-xs [&_pre_code]:leading-relaxed [&_pre_code]:text-inherit",
// Lists: logical inline padding (RTL-safe, tight so the marker sits close to
// the first word), muted markers matching the muted body, tight item gaps.
"[&_ul]:ps-4 [&_ul]:list-disc [&_ol]:ps-4 [&_ol]:list-decimal [&_li]:marker:text-muted-foreground",
"[&_li+li]:mt-2 [&_li>p]:m-0 [&_li>ul]:mt-2 [&_li>ol]:mt-2",
// GFM task lists carry their own checkbox, so drop the bullet and the indent
// that a bullet would need.
"[&_ul:has(input[type=checkbox])]:list-none [&_ul:has(input[type=checkbox])]:ps-0 [&_li:has(input[type=checkbox])]:list-none",
// Blockquote: the kit's one set-apart-prose treatment (accent rule, indent,
// muted voice), the same chrome the docs site's Callout uses. Both exist
// because they reach different content: this one styles markdown a renderer
// produced, Callout is authored directly. The values have to stay identical.
"[&_blockquote]:border-s-2 [&_blockquote]:border-accent [&_blockquote]:ps-4 [&_blockquote]:text-muted-foreground",
"[&_blockquote>*]:m-0 [&_blockquote>*+*]:mt-3",
// Tables: mirror the Table component. Ringed card container, muted micro-cap
// header, muted-foreground cells on the 24/12 padding rhythm, last row
// unbordered. Sizes to content but never wider than the measure; scrolls
// within it on narrow screens.
"[&_table]:block [&_table]:w-max [&_table]:max-w-full [&_table]:overflow-x-auto [&_table]:rounded-md [&_table]:border [&_table]:border-border [&_table]:bg-card [&_table]:shadow-xs [&_table]:text-sm",
"[&_thead]:bg-muted [&_th]:h-11 [&_th]:px-6 [&_th]:text-start [&_th]:align-middle [&_th]:font-semibold [&_th]:text-2xs [&_th]:text-muted-foreground [&_th]:uppercase [&_th]:tracking-wide [&_th]:whitespace-nowrap [&_th]:border-b [&_th]:border-border",
"[&_td]:px-6 [&_td]:py-3 [&_td]:align-middle [&_td]:text-muted-foreground [&_td]:border-b [&_td]:border-border [&_tbody_tr:last-child_td]:border-0",
// Divider: a hairline on the border token, matching Separator.
"[&_hr]:h-px [&_hr]:border-0 [&_hr]:bg-border",
// Images: responsive, with the kit's neutral hairline outline.
"[&_img]:max-w-full [&_img]:h-auto [&_img]:rounded-lg [&_img]:ring-1 [&_img]:ring-black/10 dark:[&_img]:ring-white/10",
].join(" ")
function Prose({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="prose"
className={cn(
"prose-flow max-w-measure text-sm text-muted-foreground break-words [font-synthesis:none]",
proseStyles,
className,
)}
{...props}
/>
)
}
export { Prose }