Prose
Rendered Markdown, MDX or CMS HTML in the theme's colours, styled by tag, with no classes in the content.
Install
Add the package, then two lines in your stylesheet. The second registers the package with Tailwind, adds the tokens and makes dark: class-based. The source is on GitHub.
pnpm add cennge-ui
/* app.css */
@import "tailwindcss";
@import "cennge-ui/styles.css";Peer dependencies
reactandreact-dom17, 18 or 19tailwindcss4maplibre-gl6, only for maps
To run the end-to-end tests on desktop:
pnpm exec playwright test --project=chromium --project=firefox --project=webkit- Headings, links, code, lists, tables, quotes and images are styled by their tags; the HTML needs no classes.
- Links take the accent's text colour: switch the accent at the top of the page.
- A code block wider than the column scrolls sideways and becomes a tab stop, so the keyboard can scroll it too.
Field marks
- Import
import { Prose } from "cennge-ui";- Size
- 0.7 KB gzip for this import, its dependencies aside
- Pattern
- Your HTML in a
<div>, or in your own element throughasChild - Keyboard
- Arrow left Arrow right
- SSR
- Rendered on the server in the Next.js check; hydrates without a warning
- Styles
- The
cennge-proseclass instyles.css, in the components layer - Spacing
- In
em, sosizescales the text and its spacing together - Tests
- Unit tests; end-to-end in Chromium, Firefox and WebKit, on desktop and phone viewports: prose
- Source
- src/components/prose
When to use#
Wrap Prose around content you did not write as JSX: Markdown turned into HTML, an MDX page, a post from a CMS, a README. It styles the elements that renderers produce, headings, paragraphs, lists, task lists, code, tables, quotes, details and images, in the theme's colours and with the accent on links.
For text you lay out yourself in JSX, use Tailwind classes directly; Prose adds nothing there.
Similar#
- Snippet is a single command with a copy button. Prose styles whole code blocks as part of the text, without a button.
- Kbd draws a key cap with symbols. A bare
<kbd>inside Prose gets a plain box.
HTML from a CMS or a renderer#
Prose is a <div>, so it takes dangerouslySetInnerHTML as well as children. A table keeps Markdown's column alignment and wraps long cells instead of pushing the page sideways. Only render HTML you trust or have sanitised.
0.3.0
Charts, in a new entry cennge-ui/charts, drawn in SVG with no chart library underneath.
| Import from | What it adds |
|---|---|
cennge-ui | The components |
cennge-ui/charts | BarChart, LineChart, AreaChart, DonutChart, Sparkline |
cennge-ui/map | Maps on MapLibre GL |
Series take an eight-colour palette checked for colour blindness in both themes.
import { Prose } from "cennge-ui";
// HTML as a CMS or a Markdown renderer returns it. Only render HTML you trust or have sanitised.
const releaseNotes = `
<h2>0.3.0</h2>
<p>Charts, in a new entry <code>cennge-ui/charts</code>, drawn in SVG with no chart library underneath.</p>
<table>
<thead>
<tr><th>Import from</th><th>What it adds</th></tr>
</thead>
<tbody>
<tr><td><code>cennge-ui</code></td><td>The components</td></tr>
<tr><td><code>cennge-ui/charts</code></td><td>BarChart, LineChart, AreaChart, DonutChart, Sparkline</td></tr>
<tr><td><code>cennge-ui/map</code></td><td>Maps on MapLibre GL</td></tr>
</tbody>
</table>
<blockquote><p>Series take an eight-colour palette checked for colour blindness in both themes.</p></blockquote>
`;
export default function ReleaseNotes() {
return <Prose className="w-full" dangerouslySetInnerHTML={{ __html: releaseNotes }} />;
}Sizes and your own element#
size is sm, md or lg: 14, 16 or 18 pixels. Margins and paddings are in em, so the whole block scales with it. With asChild, Prose styles your element, here an <article> and an <aside>, instead of adding a <div>.
Recolour without a rebuild
Every colour, radius and surface is a --cennge-* CSS variable. Switch the accent with one attribute: data-accent="blue", on the page or on any part of it.
import { Prose } from "cennge-ui";
export default function ThemingArticle() {
return (
<div className="grid w-full gap-8 sm:grid-cols-[minmax(0,1fr)_12rem]">
<Prose size="md" asChild>
<article>
<h3>Recolour without a rebuild</h3>
<p>
Every colour, radius and surface is a <code>--cennge-*</code> CSS variable. Switch the accent with one attribute:{" "}
<code>data-accent="blue"</code>, on the page or on any part of it.
</p>
</article>
</Prose>
<Prose size="sm" asChild>
<aside>
<h4>Presets</h4>
<ul>
<li>orange, the default</li>
<li>blue</li>
<li>green</li>
<li>violet</li>
<li>red</li>
</ul>
</aside>
</Prose>
</div>
);
}Components and classes inside#
The styles sit in Tailwind's components layer, below the utilities, so a utility class on an element inside wins over them, as text-sm text-muted does on the last line here. not-prose switches them off for a whole subtree, so a component keeps its own look.
Install
One package, with React and Tailwind CSS as its peer dependencies.
pnpm add cennge-uiReact 17, 18 and 19; Tailwind CSS 4.
import { Prose, Snippet } from "cennge-ui";
export default function InstallSection() {
return (
<Prose className="w-full max-w-prose">
<h3>Install</h3>
<p>One package, with React and Tailwind CSS as its peer dependencies.</p>
{/* not-prose: the Snippet keeps its own look instead of the prose code block's. */}
<div className="not-prose">
<Snippet>pnpm add cennge-ui</Snippet>
</div>
{/* A utility class on an element inside wins over the prose styles. */}
<p className="text-sm text-muted">React 17, 18 and 19; Tailwind CSS 4.</p>
</Prose>
);
}Without React#
The styles are a class in cennge-ui/styles.css, so server-rendered HTML can use them without the component. Add cennge-prose-sm or cennge-prose-lg for the other sizes.
<article class="cennge-prose">
<h1>Release notes</h1>
<p>…</p>
</article>The component adds one thing the class cannot: a code block that scrolls sideways becomes keyboard-focusable, and stops being a tab stop when it fits again.
Props#
Also takes every attribute of <div>.