Prose

Prose

Intent

The .prose class styles a block of bare HTML so headings, lists, blockquotes, tables, and inline elements look coherent without per-element class wiring. Drop it on the container around long-form content -- a blog post, a documentation article, a CMS-rendered page, an email body -- and the descendant typography handles itself.

Classed elements inside .prose keep their own identity: a <pre class="code"> picks up brio's code component styling rather than prose's bare-pre treatment; a <table class="table"> keeps the table component; a <details class="details"> gets brio's full details chrome. Prose only paints when no class is declared.

Brio in plain HTML

A small CSS library is mostly an opinionated stylesheet over semantic markup. The .prose class is the part that handles long-form content -- the kind of writing where you'd otherwise reach for a CMS theme or a typography reset.

Why prose

Most pages in a docs site or knowledge base aren't single components. They're paragraphs, headings, code blocks, lists, occasional images, the rare blockquote. Each could be wired up with its own class, but the volume makes that tiresome and the markup forgets itself by the third article. Bare HTML inside a single .prose wrapper is faster to write, faster to read, and survives copy-paste from external sources without re-classing every element.

What it doesn't do

Prose isn't a layout primitive. It doesn't constrain containers, build columns, or reflow content responsively beyond setting a comfortable max-line-length (65ch). For full-width docs pages -- where tables and code examples want the whole viewport -- pair with .prose.is-wide.

Lists

Unordered

  • Sets typography for headings, paragraphs, links, lists, blockquotes.
  • Styles tables and definition lists when they aren't classed.
  • Adds visual chrome to <details>, <kbd>, <abbr>, <mark>, and friends.
  • Survives nesting:
    • Indents naturally.
    • Uses muted markers.
    • And nests further:
      • Three levels deep stays readable.
      • Each level inherits the rhythm.

Ordered

  1. Author the markup.
  2. Wrap in .prose.
  3. Ship.

Task lists

  • Port v0.1 components to brio.
  • Add the v0.2 extras layer.
  • Write a real consumer site.
  • Retire the transfer manifest.

Blockquotes

The best way to predict the future is to invent it. Hype-driven front-end churn invents it for us, in the worst possible direction.

A small library you fully understand beats a large one you rent.

— Brio, internal philosophy notes

Inline elements

Inline code is set in --font-mono with a quiet --color-fill-subtle background, so references like --prose-spacing or document.querySelector() read distinctly without yelling.

Press Cmd + K to open the command palette. The Esc key dismisses it.

Abbreviations get a dotted underline and a help-cursor: CSS handles the visual layer, HTML carries the structure, and JS only enters where the platform falls short.

Use highlighted text sparingly -- it's loud enough that one or two phrases per article register; more, and the eye stops noticing.

Strikethrough text comes in two flavors: deleted content for editorial removal and obsolete syntax for things the language used to accept.

Tables

Bare tables get a quiet treatment -- no outer border, just header underline and row dividers. Use a classed <table class="table"> for the full component (with hover, striping, density, etc.).

Token Default Purpose
--prose-spacing --space-sm-md Vertical rhythm between direct children.
--prose-max-width 65ch Readable line length for long-form content.
--prose-blockquote-border 3px solid --color-border-strong Inline-start accent on blockquotes.

Definition lists

Atom
A small, self-contained component. .button, .input, .badge.
Composition primitive
Layout helper that arranges children. .cluster, .stack, .media.
Slotted container
A root with named child slots. .tabs, .timeline, .stat.

Disclosure

What's inside a bare <details>?

Prose adds a quiet border, padding, and a custom rotating chevron via ::before on the summary. The expand-collapse animation comes from brio's global details.css (which applies to every <details>); prose only adds the visual chrome.

Reach for <details class="details"> when you want brio's full component (richer chrome, token-driven theming, hover states).

Images and figures

Abstract textured rectangle, illustrative placeholder
A figure pairs an image with a caption. Prose styles the caption smaller and muted so it reads as supporting text.

Footnotes and rules

Inline references can point to numbered footnotes 1 at the end of the article. Goldmark and other GFM-flavor markdown processors emit a .footnotes section automatically.


Horizontal rules make natural breakpoints in long articles -- shifting topics, separating an aside from the main flow, marking the end of the body before footnotes. Prose gives them a quiet 1px line and generous block-margins.


  1. Footnotes use .footnotes as the section wrapper. Prose drops the font size a step so the metadata sits below the body without competing.
<article class="prose">

<h1>Brio in plain HTML</h1>

<p>
    A small CSS library is mostly an opinionated stylesheet over
    semantic markup. The <code>.prose</code> class is the part
    that handles long-form content -- the kind of writing where
    you&#39;d otherwise reach for a CMS theme or a typography reset.
</p>

<h2>Why prose</h2>

<p>
    Most pages in a docs site or knowledge base aren&#39;t single
    components. They&#39;re paragraphs, headings, code blocks, lists,
    occasional images, the rare blockquote. Each could be wired
    up with its own class, but the volume makes that tiresome and
    the markup forgets itself by the third article. Bare HTML
    inside a single <code>.prose</code> wrapper is faster to
    write, faster to read, and survives copy-paste from external
    sources without re-classing every element.
</p>

<h3>What it doesn&#39;t do</h3>

<p>
    Prose isn&#39;t a layout primitive. It doesn&#39;t constrain
    containers, build columns, or reflow content responsively
    beyond setting a comfortable max-line-length
    (<code>65ch</code>). For full-width docs pages -- where
    tables and code examples want the whole viewport -- pair with
    <code>.prose.is-wide</code>.
</p>

<h2>Lists</h2>

<h3>Unordered</h3>

<ul>
    <li>Sets typography for headings, paragraphs, links, lists, blockquotes.</li>
    <li>Styles tables and definition lists when they aren&#39;t classed.</li>
    <li>Adds visual chrome to <code>&lt;details&gt;</code>, <code>&lt;kbd&gt;</code>, <code>&lt;abbr&gt;</code>, <code>&lt;mark&gt;</code>, and friends.</li>
    <li>
        Survives nesting:
        <ul>
            <li>Indents naturally.</li>
            <li>Uses muted markers.</li>
            <li>
                And nests further:
                <ul>
                    <li>Three levels deep stays readable.</li>
                    <li>Each level inherits the rhythm.</li>
                </ul>
            </li>
        </ul>
    </li>
</ul>

<h3>Ordered</h3>

<ol>
    <li>Author the markup.</li>
    <li>Wrap in <code>.prose</code>.</li>
    <li>Ship.</li>
</ol>

<h3>Task lists</h3>

<ul>
    <li><input type="checkbox" checked=""/> Port v0.1 components to brio.</li>
    <li><input type="checkbox" checked=""/> Add the v0.2 extras layer.</li>
    <li><input type="checkbox"/> Write a real consumer site.</li>
    <li><input type="checkbox"/> Retire the transfer manifest.</li>
</ul>

<h2>Blockquotes</h2>

<blockquote>
    <p>
        The best way to predict the future is to invent it. Hype-driven
        front-end churn invents it for us, in the worst possible direction.
    </p>
    <p>
        A small library you fully understand beats a large one you
        rent.
    </p>
    <cite>— Brio, internal philosophy notes</cite>
</blockquote>

<h2>Inline elements</h2>

<p>
    Inline code is set in
    <code>--font-mono</code> with a quiet
    <code>--color-fill-subtle</code> background, so references
    like <code>--prose-spacing</code> or
    <code>document.querySelector()</code> read distinctly without
    yelling.
</p>

<p>
    Press <kbd>Cmd</kbd> + <kbd>K</kbd> to open the command
    palette. The <kbd>Esc</kbd> key dismisses it.
</p>

<p>
    Abbreviations get a dotted underline and a help-cursor:
    <abbr title="Cascading Style Sheets">CSS</abbr> handles the
    visual layer, <abbr title="HyperText Markup Language">HTML</abbr>
    carries the structure, and <abbr title="JavaScript">JS</abbr>
    only enters where the platform falls short.
</p>

<p>
    Use <mark>highlighted text</mark> sparingly -- it&#39;s loud
    enough that one or two phrases per article register; more,
    and the eye stops noticing.
</p>

<p>
    Strikethrough text comes in two flavors:
    <del>deleted content</del> for editorial removal and
    <s>obsolete syntax</s> for things the language used to
    accept.
</p>

<h2>Tables</h2>

<p>
    Bare tables get a quiet treatment -- no outer border, just
    header underline and row dividers. Use a classed
    <code>&lt;table class=&#34;table&#34;&gt;</code> for the full
    component (with hover, striping, density, etc.).
</p>

<table>
    <thead>
        <tr>
            <th>Token</th>
            <th>Default</th>
            <th>Purpose</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><code>--prose-spacing</code></td>
            <td><code>--space-sm-md</code></td>
            <td>Vertical rhythm between direct children.</td>
        </tr>
        <tr>
            <td><code>--prose-max-width</code></td>
            <td><code>65ch</code></td>
            <td>Readable line length for long-form content.</td>
        </tr>
        <tr>
            <td><code>--prose-blockquote-border</code></td>
            <td><code>3px solid --color-border-strong</code></td>
            <td>Inline-start accent on blockquotes.</td>
        </tr>
    </tbody>
</table>

<h2>Definition lists</h2>

<dl>
    <dt>Atom</dt>
    <dd>A small, self-contained component. <code>.button</code>, <code>.input</code>, <code>.badge</code>.</dd>
    <dt>Composition primitive</dt>
    <dd>Layout helper that arranges children. <code>.cluster</code>, <code>.stack</code>, <code>.media</code>.</dd>
    <dt>Slotted container</dt>
    <dd>A root with named child slots. <code>.tabs</code>, <code>.timeline</code>, <code>.stat</code>.</dd>
</dl>

<h2>Disclosure</h2>

<details>
    <summary>What&#39;s inside a bare <code>&lt;details&gt;</code>?</summary>
    <p>
        Prose adds a quiet border, padding, and a custom
        rotating chevron via <code>::before</code> on the
        summary. The expand-collapse animation comes from brio&#39;s
        global <code>details.css</code> (which applies to every
        <code>&lt;details&gt;</code>); prose only adds the
        visual chrome.
    </p>
    <p>
        Reach for <code>&lt;details class=&#34;details&#34;&gt;</code>
        when you want brio&#39;s full component (richer chrome,
        token-driven theming, hover states).
    </p>
</details>

<h2>Images and figures</h2>

<figure>
    <img src="https://picsum.photos/seed/prose-fig/800/360" alt="Abstract textured rectangle, illustrative placeholder"/>
    <figcaption>
        A figure pairs an image with a caption. Prose styles the
        caption smaller and muted so it reads as supporting text.
    </figcaption>
</figure>

<h2>Footnotes and rules</h2>

<p>
    Inline references can point to numbered footnotes
    <sup><a href="#fn-1">1</a></sup> at the end of the article.
    Goldmark and other GFM-flavor markdown processors emit a
    <code>.footnotes</code> section automatically.
</p>

<hr/>

<p>
    Horizontal rules make natural breakpoints in long
    articles -- shifting topics, separating an aside from the
    main flow, marking the end of the body before footnotes.
    Prose gives them a quiet 1px line and generous
    block-margins.
</p>

<section class="footnotes">
    <hr/>
    <ol>
        <li id="fn-1">
            Footnotes use <code>.footnotes</code> as the section
            wrapper. Prose drops the font size a step so the
            metadata sits below the body without competing.
            <a href="#" aria-label="Back to reference">↩</a>
        </li>
    </ol>
</section>

</article>

Mixing prose with brio components

Prose's element selectors are scoped with :not([class]) on the elements where consumers routinely add classes (anchors, lists, tables). A classed component inside .prose keeps its own styling.

Inside a prose block

A bare paragraph and a bare list look like prose. Drop a classed component in and it keeps its identity:

Class Wins
<table class="table"> Brio's table component (this row).
<table> Prose's quiet treatment (the table above).

Same rule for unordered lists -- a classed <ul class="cluster"> stays a horizontal cluster instead of a vertical disc list.

  • Composition wins
  • Even inside prose
  • No specificity fight

Code blocks pick up brio's code component -- header, syntax tokens, copy button -- when they have the class:

<article class="prose">
    <h2>A heading</h2>
    <p>A paragraph</p>
</article>
<article class="prose">

<h2>Inside a prose block</h2>

<p>
    A bare paragraph and a bare list look like prose. Drop a
    classed component in and it keeps its identity:
</p>

<table class="table is-striped is-bordered">
    <thead>
        <tr>
            <th>Class</th>
            <th>Wins</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><code>&lt;table class=&#34;table&#34;&gt;</code></td>
            <td>Brio&#39;s table component (this row).</td>
        </tr>
        <tr>
            <td><code>&lt;table&gt;</code></td>
            <td>Prose&#39;s quiet treatment (the table above).</td>
        </tr>
    </tbody>
</table>

<p>
    Same rule for unordered lists -- a classed
    <code>&lt;ul class=&#34;cluster&#34;&gt;</code> stays a horizontal
    cluster instead of a vertical disc list.
</p>

<ul class="cluster">
    <li><span class="badge is-success">Composition wins</span></li>
    <li><span class="badge is-accent">Even inside prose</span></li>
    <li><span class="badge is-warning">No specificity fight</span></li>
</ul>

<p>
    Code blocks pick up brio&#39;s
    <a href="/brio/components/code">code component</a> -- header,
    syntax tokens, copy button -- when they have the class:
</p>

<pre class="code"><code class="language-html">&lt;article class=&#34;prose&#34;&gt;
    &lt;h2&gt;A heading&lt;/h2&gt;
    &lt;p&gt;A paragraph&lt;/p&gt;
&lt;/article&gt;</code></pre>

</article>

.is-wide

The default --prose-max-width is 65ch -- comfortable for body copy. Reference pages, dashboards, and any context where tables or wide code samples need the full container should opt into .prose.is-wide, which removes the inline-size cap.

Wide prose

Wide prose still inherits the typography rhythm, list styling, and inline-element chrome -- it just doesn't clamp the inline-size. Use on pages where the content has its own width discipline (cards, grids, <docs-resize> demos), and the prose clamp would awkwardly orphan the wide content.

<article class="prose is-wide">
    <h3>Wide prose</h3>
    <p>
        Wide prose still inherits the typography rhythm, list
        styling, and inline-element chrome -- it just doesn&#39;t
        clamp the inline-size. Use on pages where the content
        has its own width discipline (cards, grids,
        <code>&lt;docs-resize&gt;</code> demos), and the prose
        clamp would awkwardly orphan the wide content.
    </p>
</article>

Customization

Eight scoped tokens shape prose: --prose-spacing, --prose-max-width, --prose-border, --prose-blockquote-border, --prose-kbd-border, --prose-mark-bg, and --prose-figcaption-align. Override on the .prose root for per-instance tuning, or on :root to retone every prose block in a project.

All element selectors inside prose are wrapped in :where() so they contribute zero specificity beyond the .prose class itself. Same-layer component rules override prose without specificity gymnastics, and component classes win automatically when used inside prose blocks.

Markup

<!-- Long-form article -->
<article class="prose">
    <h1>Title</h1>
    <p>Body content...</p>
    <h2>Section</h2>
    <p>...</p>
</article>

<!-- Reference / docs page (full-width) -->
<article class="prose is-wide">
    <h2>Reference</h2>
    <table class="table">...</table>
    <pre class="code">...</pre>
</article>

CSS Reference: Prose

Long-form content styling for bare HTML

Source: src/css/prose.css

Tokens

TokenDefaultDescription
--prose-spacingvar(--space-sm-md)Vertical rhythm between direct children
--prose-max-width65chMaximum inline size for readable line length
--prose-bordervar(--border-width-default) solid var(--color-border)Separator border for rules, table rows, and details
--prose-blockquote-border3px solid var(--color-border-strong)Inline-start accent border for blockquotes
--prose-kbd-bordervar(--border-width-default) solid var(--color-border-strong)Border for keyboard input elements
--prose-figcaption-alignstartText alignment for figure captions
--prose-mark-bgvar(--color-warning-subtleBackground color for highlighted text

Classes

ClassDescription
.prosecomponent root
.is-wideRemoves the max-width limit for full-width layouts