RichTextProse

A semantic prose renderer for paragraphs, lists, and inline marks. Pass any tree of <p>, <ul>, <strong>, <em>, <code>, and <a> and the component paints it with editorial typography against cb-* tokens.

Distinct from RichTextNodes (token-array renderer in @craft-bits/core/text/rich-text) and Prose (full-document recipe in @craft-bits/core/edu/prose). This is the inline + short-block sweet spot for narration, lesson copy, and callout bodies.

Preview

The dot product

The dot product is the smallest brick in the stack. Almost every later result is a dot product wearing a different hat — a projection, a cosine similarity, or a weighted attention score.

  • Projecting v onto a unit vector is a single dot product.
  • Two vectors are orthogonal exactly when their dot product is zero.

Drop semantic HTML inside and the recipe paints it with editorial typography — serif italic, mono-chip code, accent underlines.

Customize
Root element
div
Density
Headings

Installation

npx shadcn@latest add https://craftbits.dev/r/rich-text-prose.json

Usage

import { RichTextProse } from "@craft-bits/core";
 
<RichTextProse>
  <p>
    The <strong>dot product</strong> is the smallest brick in the stack.
    Almost every later result is a dot product wearing a different hat — a{" "}
    <em>projection</em>, a <code>cosine similarity</code>, or a{" "}
    <a href="/notes/attention">weighted attention score</a>.
  </p>
  <ul>
    <li>Projection onto a unit vector is a single dot product.</li>
    <li>Two vectors are orthogonal exactly when their dot product is zero.</li>
  </ul>
</RichTextProse>

Prefer the subpath import when you only want this primitive:

import { RichTextProse } from "@craft-bits/core/text/rich-text-prose";

Inline vocabulary

TagRenders asNotes
<p>body paragraphFirst paragraph uses full cb-fg; siblings are cb-fg-muted.
<strong> / <b>font-semiboldEmphasis weight tied to cb-fg.
<em> / <i>serif italicEditorial aside.
<code>mono + bordered chipInline code against cb-bg-muted.
<a>accent underlineVisible :focus-visible outline.
<mark>tinted highlightcb-accent/15 background, cb-fg text.
<small>0.875em mutedFootnote-style aside.
<ul> / <ol>bulleted / numberedMarkers tinted cb-fg-subtle.

Variants

Switch the root element when the surrounding layout expects a block-level node:

<RichTextProse as="section">{children}</RichTextProse>

Tighten the rhythm for dense reference reading:

<RichTextProse density="compact">{children}</RichTextProse>

Promote headings to display-italic serif when the children include short essays:

<RichTextProse headingScale="editorial">
  <h2>A short essay</h2>
  <p></p>
</RichTextProse>

Props

PropTypeDefaultDescription
childrenReactNoderequiredSemantic prose children.
as'div' | 'span' | 'p' | 'section' | 'article''div'Root element.
density'cozy' | 'compact''cozy'Inter-block rhythm.
headingScale'inline' | 'editorial''inline'Heading treatment when children include <h2> / <h3>.
classNamestringMerged onto the root via cn().

Accessibility

  • Pure typography — no interactive controls beyond optional anchors.
  • Anchor focus is visible: :focus-visible paints a 2px outline tied to --cb-accent.
  • Heading levels (<h2> / <h3>) keep their semantics so screen-reader heading navigation still works.
  • Body text targets WCAG AA contrast (~4.5:1) against --cb-bg in both themes.
  • No motion — no prefers-reduced-motion fallback required.

Credits

  • Extracted from: AlgoFlashcards (src/platform/ui/RichText.tsx). The source was a single-string parser with 19 lesson-specific matchers (array literals, complexity notation, DSA vocabulary icons, comparison-operator typography) plus static / reveal / interactive motion variants. The library version flips the shape — input is real semantic HTML, motion is the parent's job, and the markup vocabulary collapses to the standard inline tags rendered against cb-* tokens.