Skip to documentation content

Honest Prose

Style rendered Markdown and plain HTML with one class, in any project, including text that is still streaming.

prose-demo

Use the panel below the preview to adjust typography or start from the Chat, Document, and Compact presets. Advanced options override theme colors and code styling. Reset restores the Chat preset and theme defaults. Choose CSS or Tailwind under Export, then copy the current settings. Text width is measured in ch, approximately the width of the font's zero character.

Overview

Prose styles rendered Markdown and plain HTML. Use the honest-prose class with its stylesheet, or install the Tailwind utilities to get the styles without another CSS file.

It is plain CSS, so it works with any Markdown renderer and in a project without React or Tailwind. In an Honest UI project it reads the theme's tokens. Anywhere else it falls back to values based on the text color around it, so it follows your light and dark themes.

Markdown and Streaming Text use it for their text.

Installation

Tailwind v4, without a separate stylesheet

npx honestui@latest add https://ai.honestui.com/r/prose-tailwind.json

This copies lib/prose-tailwind.ts, with no package or stylesheet dependencies. Your project needs Tailwind v4 already configured.

import { proseTailwind } from "@/lib/prose-tailwind"

<article className={proseTailwind}>
  <h2>Release checklist</h2>
  <p>The export fix is ready.</p>
</article>

The utilities use the same --prose-* variables and not-prose opt-out as the stylesheet. Set customized variables with an inline style, or choose Tailwind in the playground and paste the copied classes directly into your template.

Keep the installed file and any pasted classes inside Tailwind's source scan. If your lib folder is excluded, add @source "../lib/prose-tailwind.ts"; to your existing Tailwind stylesheet, adjusting the path. Tailwind cannot discover class names assembled from values at runtime; copy the generated classes into source or use proseTailwind with inline variables.

The generated utilities use Tailwind's important modifier so theme element rules cannot override them. Customize with the variables. Tailwind v3 is not supported by this install option.

lib/prose-tailwind.ts
// Generated from registry/styles/prose.css by npm run prose:generate.
// Tailwind v4: keep this file inside your source scan. No extra stylesheet is needed.

/** Complete prose styles. Customize with the --prose-* variables. */
export const proseTailwind = [
  "[--prose-internal-size:var(--prose-size,var(--hui-font-size-regular,1em))]!",
  "[--prose-internal-leading:var(--prose-leading,1.6)]!",
  "[--prose-internal-flow:var(--prose-flow,var(--hui-space-4,0.85em))]!",
  "[--prose-internal-heading:var(--prose-heading,1.143em)]!",
  "[--prose-internal-color:var(--prose-color,var(--hui-color-foreground-base-primary,currentColor))]!",
  "[--prose-internal-muted:var(--prose-muted,var(--hui-color-foreground-base-secondary,color-mix(in_oklab,currentColor_72%,transparent)))]!",
  "[--prose-internal-surface:var(--prose-surface,var(--hui-color-background-base-secondary,color-mix(in_oklab,currentColor_7%,transparent)))]!",
  "[--prose-internal-rule:var(--prose-rule,var(--hui-color-border-base-primary,color-mix(in_oklab,currentColor_18%,transparent)))]!",
  "[--prose-internal-rule-strong:var(--prose-rule-strong,var(--hui-color-border-base-secondary,color-mix(in_oklab,currentColor_34%,transparent)))]!",
  "[--prose-internal-font-mono:var(--prose-font-mono,var(--hui-font-mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace))]!",
  "[--prose-internal-radius:var(--prose-radius,var(--hui-radius-1,0.25em))]!",
  "[min-width:0]!",
  "[max-width:var(--prose-width,none)]!",
  "[overflow-wrap:break-word]!",
  "[color:var(--prose-internal-color)]!",
  "[font-size:var(--prose-internal-size)]!",
  "[line-height:var(--prose-internal-leading)]!",
  "[letter-spacing:var(--prose-tracking,var(--hui-letter-spacing-regular,normal))]!",
  "[&_:where(p,ul,ol,dl,blockquote,pre,table,figure,details,hr,.prose-scroll,.not-prose):not(:where(.not-prose_*))]:[margin-block:var(--prose-internal-flow)0]!",
  "[&_:where(p,ul,ol,dl,blockquote,pre,figure):not(:where(.not-prose,.not-prose_*))]:[margin-inline:0]!",
  "[&_:where(h1,h2,h3,h4,h5,h6):not(:where(.not-prose,.not-prose_*))]:[margin-block:calc(var(--prose-internal-flow)*_1.667)0]!",
  "[&_:where(h1,h2,h3,h4,h5,h6):not(:where(.not-prose,.not-prose_*))]:[font-family:inherit]!",
  "[&_:where(h1,h2,h3,h4,h5,h6):not(:where(.not-prose,.not-prose_*))]:[font-size:1em]!",
  "[&_:where(h1,h2,h3,h4,h5,h6):not(:where(.not-prose,.not-prose_*))]:[font-weight:var(--hui-font-weight-semibold,600)]!",
  "[&_:where(h1,h2,h3,h4,h5,h6):not(:where(.not-prose,.not-prose_*))]:[line-height:1.43]!",
  "[&_:where(h1,h2,h3,h4,h5,h6):not(:where(.not-prose,.not-prose_*))]:[letter-spacing:inherit]!",
  "[&_:where(h1,h2,h3,h4,h5,h6):not(:where(.not-prose,.not-prose_*))]:[text-wrap:balance]!",
  "[&&_:where(h1,h2):not(:where(.not-prose,.not-prose_*))]:[font-size:var(--prose-internal-heading)]!",
  "[&&_:where(h1,h2):not(:where(.not-prose,.not-prose_*))]:[line-height:1.5]!",
  "[&&_:where(h1,h2,h3,h4,h5,h6):not(:where(.not-prose,.not-prose_*))+_*]:[margin-block-start:calc(var(--prose-internal-flow)*_0.667)]!",
  "[&_:where(a):not(:where(.not-prose,.not-prose_*))]:[color:inherit]!",
  "[&_:where(a):not(:where(.not-prose,.not-prose_*))]:[text-decoration:underline]!",
  "[&_:where(a):not(:where(.not-prose,.not-prose_*))]:[text-underline-offset:2px]!",
  "[&_:where(strong,b):not(:where(.not-prose,.not-prose_*))]:[font-weight:var(--hui-font-weight-semibold,600)]!",
  "[&_:where(code,kbd,samp):not(:where(.not-prose,.not-prose_*))]:[font-family:var(--prose-internal-font-mono)]!",
  "[&_:where(code,kbd,samp):not(:where(.not-prose,.not-prose_*))]:[font-size:0.9em]!",
  "[&_:where(:not(pre)>_code,kbd):not(:where(.not-prose,.not-prose_*))]:[border-radius:var(--prose-internal-radius)]!",
  "[&_:where(:not(pre)>_code,kbd):not(:where(.not-prose,.not-prose_*))]:[background:var(--prose-internal-surface)]!",
  "[&_:where(:not(pre)>_code,kbd):not(:where(.not-prose,.not-prose_*))]:[padding-inline:0.3em]!",
  "[&_:where(kbd):not(:where(.not-prose,.not-prose_*))]:[border:0.5px_solid_var(--prose-internal-rule-strong)]!",
  "[&_:where(ul,ol):not(:where(.not-prose,.not-prose_*))]:[padding-inline-start:1.45em]!",
  "[&_:where(ul):not(:where(.not-prose,.not-prose_*))]:[list-style:disc]!",
  "[&_:where(ol):not(:where(.not-prose,.not-prose_*))]:[list-style:decimal]!",
  "[&&_:where(ul_ul):not(:where(.not-prose,.not-prose_*))]:[list-style:circle]!",
  "[&_:where(li):not(:where(.not-prose,.not-prose_*))]:[margin-block:calc(var(--prose-internal-flow)/_6)0]!",
  "[&&_:where(li)>_:where(ul,ol):not(:where(.not-prose,.not-prose_*))]:[margin-block-start:calc(var(--prose-internal-flow)/_6)]!",
  "[&&_:where(ul.contains-task-list):not(:where(.not-prose,.not-prose_*))]:[list-style:none]!",
  "[&&_:where(ul.contains-task-list):not(:where(.not-prose,.not-prose_*))]:[padding-inline-start:0]!",
  "[&_:where(li.task-list-item_input):not(:where(.not-prose,.not-prose_*))]:[margin:0]!",
  "[&_:where(li.task-list-item_input):not(:where(.not-prose,.not-prose_*))]:[margin-inline-end:0.4em]!",
  "[&_:where(dt):not(:where(.not-prose,.not-prose_*))]:[font-weight:var(--hui-font-weight-medium,500)]!",
  "[&_:where(dd):not(:where(.not-prose,.not-prose_*))]:[margin:0]!",
  "[&_:where(dd):not(:where(.not-prose,.not-prose_*))]:[color:var(--prose-internal-muted)]!",
  "[&_:where(blockquote):not(:where(.not-prose,.not-prose_*))]:[border-inline-start:2px_solid_var(--prose-internal-rule-strong)]!",
  "[&_:where(blockquote):not(:where(.not-prose,.not-prose_*))]:[padding-inline-start:var(--prose-internal-flow)]!",
  "[&_:where(blockquote):not(:where(.not-prose,.not-prose_*))]:[color:var(--prose-internal-muted)]!",
  "[&&_:where(hr):not(:where(.not-prose,.not-prose_*))]:[margin-block-start:calc(var(--prose-internal-flow)*_1.333)]!",
  "[&&_:where(hr):not(:where(.not-prose,.not-prose_*))]:[border:0]!",
  "[&&&_:where(hr):not(:where(.not-prose,.not-prose_*))]:[border-block-start:0.5px_solid_var(--prose-internal-rule)]!",
  "[&&_:where(hr):not(:where(.not-prose,.not-prose_*))]:[height:0]!",
  "[&_:where(pre):not(:where(.not-prose,.not-prose_*))]:[overflow-x:auto]!",
  "[&_:where(pre):not(:where(.not-prose,.not-prose_*))]:[border-radius:calc(var(--prose-internal-radius)*_3)]!",
  "[&_:where(pre):not(:where(.not-prose,.not-prose_*))]:[background:var(--prose-internal-surface)]!",
  "[&_:where(pre):not(:where(.not-prose,.not-prose_*))]:[padding:var(--prose-internal-flow)]!",
  "[&_:where(pre):not(:where(.not-prose,.not-prose_*))]:[line-height:1.65]!",
  "[&_:where(.prose-scroll):not(:where(.not-prose,.not-prose_*))]:[overflow-x:auto]!",
  "[&_:where(table):not(:where(.not-prose,.not-prose_*))]:[width:100%]!",
  "[&_:where(table):not(:where(.not-prose,.not-prose_*))]:[border-collapse:collapse]!",
  "[&_:where(table):not(:where(.not-prose,.not-prose_*))]:[font-size:0.857em]!",
  "[&_:where(table):not(:where(.not-prose,.not-prose_*))]:[line-height:1.34]!",
  "[&&_:where(.prose-scroll)>_:where(table)]:[margin-block-start:0]!",
  "[&_:where(th,td):not(:where(.not-prose,.not-prose_*))]:[padding:calc(var(--prose-internal-flow)/_3)calc(var(--prose-internal-flow)*_0.667)]!",
  "[&_:where(th,td):not(:where(.not-prose,.not-prose_*))]:[vertical-align:top]!",
  "[&_:where(th,td):not(:where(.not-prose,.not-prose_*))]:[text-align:start]!",
  "[&_:where(th):not(:where(.not-prose,.not-prose_*))]:[font-weight:var(--hui-font-weight-medium,500)]!",
  "[&&_:where(td):not(:where(.not-prose,.not-prose_*))]:[border-block-start:0.5px_solid_var(--prose-internal-rule)]!",
  "[&_:where(img,video):not(:where(.not-prose,.not-prose_*))]:[max-width:100%]!",
  "[&_:where(img,video):not(:where(.not-prose,.not-prose_*))]:[height:auto]!",
  "[&_:where(figcaption):not(:where(.not-prose,.not-prose_*))]:[margin-block-start:calc(var(--prose-internal-flow)/_2)]!",
  "[&_:where(figcaption):not(:where(.not-prose,.not-prose_*))]:[color:var(--prose-internal-muted)]!",
  "[&_:where(figcaption):not(:where(.not-prose,.not-prose_*))]:[font-size:0.857em]!",
  "[&&&_>_:where(:first-child)]:[margin-block-start:0]!",
  "[&&&_:where(li,blockquote,td,th,dd,figure)>_:where(:first-child):not(:where(.not-prose_*))]:[margin-block-start:0]!",
].join(" ")

/** Static classes for copying into a template; paste the result into scanned source. */
export function proseTailwindClasses(settings: Record<string, string> = {}) {
  const variables = Object.entries(settings).map(([name, value]) => {
    if (!/^--prose-[a-z-]+$/.test(name) || /[;{}\[\]\\]/.test(value)) {
      throw new Error("Invalid prose setting.")
    }
    return `[${name}:${value.replaceAll("_", "\\_").replace(/\s+/g, "_")}]`
  })
  return [proseTailwind, ...variables].join(" ")
}

Standalone CSS

1
Add the file.
npx honestui@latest add https://ai.honestui.com/r/prose.json

Or copy it into styles/prose.css yourself:

styles/prose.css
/*
 * Prose styles rendered Markdown and plain HTML: put `class="honest-prose"` on
 * the element that wraps it. It is plain CSS and needs no build step.
 *
 * Set these on `.honest-prose`, or on any ancestor, to change it:
 *
 *   --prose-size      Body text size.
 *   --prose-width     Maximum text width.
 *   --prose-leading   Body line height, as a number.
 *   --prose-flow      Space between blocks.
 *   --prose-heading   Size of first- and second-level headings.
 *   --prose-tracking  Body letter spacing.
 *   --prose-color, --prose-muted, --prose-surface,
 *   --prose-rule, --prose-rule-strong
 *   --prose-font-mono, --prose-radius
 *
 * Each one falls back to an Honest UI token, then to a value that works with
 * no tokens at all. Prose never sets them itself, so a value from a utility
 * class or an inline style always applies.
 *
 * Add `class="not-prose"` to anything inside that brings its own styles,
 * such as a component. It keeps its place in the flow and nothing else.
 *
 * Blocks are spaced with a top margin only, and no rule depends on what comes
 * after an element, so text that is still arriving does not move what is
 * already on screen.
 */

.honest-prose {
  --_size: var(--prose-size, var(--hui-font-size-regular, 1em));
  --_leading: var(--prose-leading, 1.6);
  --_flow: var(--prose-flow, var(--hui-space-4, 0.85em));
  --_heading: var(--prose-heading, 1.143em);
  --_color: var(--prose-color, var(--hui-color-foreground-base-primary, currentColor));
  --_muted: var(
    --prose-muted,
    var(--hui-color-foreground-base-secondary, color-mix(in oklab, currentColor 72%, transparent))
  );
  --_surface: var(
    --prose-surface,
    var(--hui-color-background-base-secondary, color-mix(in oklab, currentColor 7%, transparent))
  );
  --_rule: var(
    --prose-rule,
    var(--hui-color-border-base-primary, color-mix(in oklab, currentColor 18%, transparent))
  );
  --_rule-strong: var(
    --prose-rule-strong,
    var(--hui-color-border-base-secondary, color-mix(in oklab, currentColor 34%, transparent))
  );
  --_font-mono: var(
    --prose-font-mono,
    var(--hui-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace)
  );
  --_radius: var(--prose-radius, var(--hui-radius-1, 0.25em));

  min-width: 0;
  max-width: var(--prose-width, none);
  overflow-wrap: break-word;
  color: var(--_color);
  font-size: var(--_size);
  line-height: var(--_leading);
  letter-spacing: var(--prose-tracking, var(--hui-letter-spacing-regular, normal));
}

/* Flow */

.honest-prose
  :where(p, ul, ol, dl, blockquote, pre, table, figure, details, hr, .prose-scroll, .not-prose):not(
    :where(.not-prose *)
  ) {
  margin-block: var(--_flow) 0;
}

.honest-prose :where(p, ul, ol, dl, blockquote, pre, figure):not(:where(.not-prose, .not-prose *)) {
  margin-inline: 0;
}

/* Headings */

.honest-prose :where(h1, h2, h3, h4, h5, h6):not(:where(.not-prose, .not-prose *)) {
  margin-block: calc(var(--_flow) * 1.667) 0;
  font-family: inherit;
  font-size: 1em;
  font-weight: var(--hui-font-weight-semibold, 600);
  line-height: 1.43;
  letter-spacing: inherit;
  text-wrap: balance;
}

.honest-prose :where(h1, h2):not(:where(.not-prose, .not-prose *)) {
  font-size: var(--_heading);
  line-height: 1.5;
}

/* A heading and the block under it read as one unit. */
.honest-prose :where(h1, h2, h3, h4, h5, h6):not(:where(.not-prose, .not-prose *)) + * {
  margin-block-start: calc(var(--_flow) * 0.667);
}

/* Inline text */

.honest-prose :where(a):not(:where(.not-prose, .not-prose *)) {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.honest-prose :where(strong, b):not(:where(.not-prose, .not-prose *)) {
  font-weight: var(--hui-font-weight-semibold, 600);
}

.honest-prose :where(code, kbd, samp):not(:where(.not-prose, .not-prose *)) {
  font-family: var(--_font-mono);
  font-size: 0.9em;
}

.honest-prose :where(:not(pre) > code, kbd):not(:where(.not-prose, .not-prose *)) {
  border-radius: var(--_radius);
  background: var(--_surface);
  padding-inline: 0.3em;
}

.honest-prose :where(kbd):not(:where(.not-prose, .not-prose *)) {
  border: 0.5px solid var(--_rule-strong);
}

/* Lists */

.honest-prose :where(ul, ol):not(:where(.not-prose, .not-prose *)) {
  padding-inline-start: 1.45em;
}

.honest-prose :where(ul):not(:where(.not-prose, .not-prose *)) {
  list-style: disc;
}

.honest-prose :where(ol):not(:where(.not-prose, .not-prose *)) {
  list-style: decimal;
}

.honest-prose :where(ul ul):not(:where(.not-prose, .not-prose *)) {
  list-style: circle;
}

.honest-prose :where(li):not(:where(.not-prose, .not-prose *)) {
  margin-block: calc(var(--_flow) / 6) 0;
}

.honest-prose :where(li) > :where(ul, ol):not(:where(.not-prose, .not-prose *)) {
  margin-block-start: calc(var(--_flow) / 6);
}

/* A task list's checkboxes stand in for the bullets. */
.honest-prose :where(ul.contains-task-list):not(:where(.not-prose, .not-prose *)) {
  list-style: none;
  padding-inline-start: 0;
}

.honest-prose :where(li.task-list-item input):not(:where(.not-prose, .not-prose *)) {
  margin: 0;
  margin-inline-end: 0.4em;
}

.honest-prose :where(dt):not(:where(.not-prose, .not-prose *)) {
  font-weight: var(--hui-font-weight-medium, 500);
}

.honest-prose :where(dd):not(:where(.not-prose, .not-prose *)) {
  margin: 0;
  color: var(--_muted);
}

/* Quotes and rules */

.honest-prose :where(blockquote):not(:where(.not-prose, .not-prose *)) {
  border-inline-start: 2px solid var(--_rule-strong);
  padding-inline-start: var(--_flow);
  color: var(--_muted);
}

.honest-prose :where(hr):not(:where(.not-prose, .not-prose *)) {
  margin-block-start: calc(var(--_flow) * 1.333);
  border: 0;
  border-block-start: 0.5px solid var(--_rule);
  height: 0;
}

/* Code blocks, for a renderer that does not bring a component for them */

.honest-prose :where(pre):not(:where(.not-prose, .not-prose *)) {
  overflow-x: auto;
  border-radius: calc(var(--_radius) * 3);
  background: var(--_surface);
  padding: var(--_flow);
  line-height: 1.65;
}

/* Tables */

/* Wrap a table in `.prose-scroll` so a wide one scrolls instead of the page. */
.honest-prose :where(.prose-scroll):not(:where(.not-prose, .not-prose *)) {
  overflow-x: auto;
}

.honest-prose :where(table):not(:where(.not-prose, .not-prose *)) {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.857em;
  line-height: 1.34;
}

.honest-prose :where(.prose-scroll) > :where(table) {
  margin-block-start: 0;
}

.honest-prose :where(th, td):not(:where(.not-prose, .not-prose *)) {
  padding: calc(var(--_flow) / 3) calc(var(--_flow) * 0.667);
  vertical-align: top;
  text-align: start;
}

.honest-prose :where(th):not(:where(.not-prose, .not-prose *)) {
  font-weight: var(--hui-font-weight-medium, 500);
}

/* The line is drawn above each body cell, so a row that arrives later needs no restyle. */
.honest-prose :where(td):not(:where(.not-prose, .not-prose *)) {
  border-block-start: 0.5px solid var(--_rule);
}

/* Media */

.honest-prose :where(img, video):not(:where(.not-prose, .not-prose *)) {
  max-width: 100%;
  height: auto;
}

.honest-prose :where(figcaption):not(:where(.not-prose, .not-prose *)) {
  margin-block-start: calc(var(--_flow) / 2);
  color: var(--_muted);
  font-size: 0.857em;
}

/* Last, so it wins over the flow rules above: nothing is pushed off the top of its container. */
.honest-prose > :where(:first-child),
.honest-prose :where(li, blockquote, td, th, dd, figure) > :where(:first-child):not(:where(.not-prose *)) {
  margin-block-start: 0;
}
2
Import it in your global stylesheet. With Tailwind, put it after the Tailwind import.
@import "tailwindcss";
@import "../styles/prose.css";

Usage

<article class="honest-prose">
  <h2>Release checklist</h2>
  <p>The export fix is ready.</p>
</article>

With a Markdown renderer, wrap its output:

<div className="honest-prose">
  <ReactMarkdown>{text}</ReactMarkdown>
</div>

Variables

Three variables do most of the work. Set them on the .honest-prose element or on anything around it.

VariableSetsDefault
--prose-sizeBody text size--hui-font-size-regular, or 1em
--prose-leadingBody line height, as a number1.6
--prose-flowSpace between blocks--hui-space-4, or 0.85em
--prose-widthMaximum text widthNone; the surrounding layout sets the width

The rest are for matching a theme:

VariableSetsDefault
--prose-headingSize of first- and second-level headings1.143em
--prose-trackingBody letter spacing--hui-letter-spacing-regular, or normal
--prose-colorText--hui-color-foreground-base-primary, or the current color
--prose-mutedQuotes and captions--hui-color-foreground-base-secondary, or 72% of the current color
--prose-surfaceBehind inline code and plain code blocks--hui-color-background-base-secondary, or 7% of the current color
--prose-ruleTable lines and horizontal rules--hui-color-border-base-primary, or 18% of the current color
--prose-rule-strongThe rule beside a quote--hui-color-border-base-secondary, or 34% of the current color
--prose-font-monoCode--hui-font-mono, or the system monospace font
--prose-radiusCorners of inline code--hui-radius-1, or 0.25em

Prose reads these variables and never sets them. A value from a class, an inline style, or an ancestor always applies:

<div className="honest-prose [--prose-size:16px] [--prose-leading:1.7] [--prose-flow:18px]">…</div>

You can have several prose styles in one app: a tight one for chat replies and a roomier one for documents, each a different set of values on the same class.

The fallback colors use color-mix(). If you set your own contrast-sensitive colors, check them: the muted color carries text.

Behavior

Sizes

Headings are small on purpose. A reply sits inside a page that already has a title, so first- and second-level headings are a little larger than body text, and the rest match it in a heavier weight. For a document with a real title, raise --prose-heading or style h1 yourself.

Streaming

Blocks are spaced with a margin above them, never below. No rule depends on what comes after an element. A paragraph, list item, or table row that arrives later does not restyle what is already on screen.

Opting out

Add class="not-prose" to an element that brings its own styles, such as a component placed between paragraphs. It keeps its space in the flow, and no other rule reaches it or anything inside it. Code View carries this class itself.

Wide tables

Wrap a table in an element with class="prose-scroll", so a wide table scrolls sideways instead of widening the page. Make that wrapper focusable and give it a name, so a keyboard user can scroll it. Markdown does both.

Utility classes

Prose's rules sit outside Tailwind's layers, so a utility for a property Prose sets has no effect on the same element. text-sm on the .honest-prose element does not change its size. Use the variables, or your own CSS written after the import. Utilities for anything Prose does not set, such as width or padding on the wrapper, work as usual.

It is written this way because a theme's own heading rules also sit outside the layers, and would otherwise win.

Accessibility

Prose changes how elements look and leaves what they are alone. Headings, lists, tables, and links keep their roles.

Links are underlined, so they do not depend on color. Line height is a number without a unit, and spacing uses margins, so text a reader has enlarged or respaced is not clipped.

The preview has been checked with axe and keyboard controls in Chromium. Standalone HTML has also been checked without theme tokens in light and dark colors, at a 320px viewport, with text-spacing overrides, and with forced-colors emulation. Screen-reader use and actual 400% browser zoom have not been checked yet.

What it styles

ElementResult
h1 to h6Semibold, balanced wrapping, closer to the block below
p, ul, ol, dl, blockquote, pre, table, figure, details, hrSpaced by --prose-flow
ul, olDisc and decimal markers, a circle for a nested list, tighter items
A task list from GitHub Flavored MarkdownNo bullets, the checkbox in their place
aUnderlined, in the text color
strong, bSemibold
code, kbd, sampMonospace, on the surface color when inline
preThe surface color, padded, scrolling sideways
tableSmaller text, a line above each body row
img, videoNo wider than the container