Typeset
Prose styling for long-form and rendered-markdown content.
Installation
Included in poetry-ui — available as
poetry_typeset
the moment you've installed Poetry,
with no per-component step. To own the source and edit it, copy it into your app:
bin/rails g poetry:add typeset
Default
The Pilcrow Papers
You render markdown and get back plain, unstyled HTML. Typeset styles everything inside the container from three rhythm variables, and everything else derives.
Rhythm
Three controls: --typeset-size, --typeset-leading,
and --typeset-flow.
- It fits its container — chat bubble or article.
- It reads your theme tokens, so dark mode is free.
- Appending a block never restyles earlier blocks.
Utilities on an element still win — the rules are zero-specificity.
| Control | Default |
|---|---|
| size | 1em |
| leading | 1.75 |
| flow | 1.25em |
<%# Rendered-markdown fixture: bare elements, styled entirely by the
app-owned typeset.css. %>
<%= poetry_typeset(class: "max-w-xl") do %>
<h1>The Pilcrow Papers</h1>
<p>You render markdown and get back plain, unstyled HTML. <strong>Typeset</strong>
styles everything inside the container from three rhythm variables, and
<a href="#preview">everything else derives</a>.</p>
<h2>Rhythm</h2>
<p>Three controls: <code>--typeset-size</code>, <code>--typeset-leading</code>,
and <code>--typeset-flow</code>.</p>
<ul>
<li>It fits its container — chat bubble or article.</li>
<li>It reads your theme tokens, so dark mode is free.</li>
<li>Appending a block never restyles earlier blocks.</li>
</ul>
<blockquote><p>Utilities on an element still win — the rules are zero-specificity.</p></blockquote>
<table>
<thead><tr><th>Control</th><th>Default</th></tr></thead>
<tbody>
<tr><td>size</td><td>1em</td></tr>
<tr><td>leading</td><td>1.75</td></tr>
<tr><td>flow</td><td>1.25em</td></tr>
</tbody>
</table>
<% end %>
Chat preset
Here's the migration plan:
- Add the
statuscolumn. - Backfill in batches.
bin/rails db:migrate
Earlier blocks keep their styling as new ones stream in.
<%# The container-relative pitch: the same markdown follows the tighter
type around it. A preset is a tiny class — here inline vars stand in
for a .typeset-chat preset in your CSS. %>
<div class="max-w-sm rounded-lg border bg-muted/30 p-4 text-sm">
<%= poetry_typeset(class: "[--typeset-flow:0.75em] [--typeset-leading:1.6]") do %>
<p>Here's the migration plan:</p>
<ol>
<li>Add the <code>status</code> column.</li>
<li>Backfill in batches.</li>
</ol>
<pre><code>bin/rails db:migrate</code></pre>
<p>Earlier blocks keep their styling as new ones stream in.</p>
<% end %>
</div>
Docs preset
Deploying for the first time
This guide walks through an initial production deploy. Every step is idempotent, so a run that fails partway is always safe to repeat.
Before you start
You will need a provisioned host and push access to the release branch.
- Prepare the database with
bin/rails db:prepare. - Set
SECRET_KEY_BASEin the server environment. - Point the load balancer at the freshly booted instances.
The docs preset widens the measure and opens up the spacing,
which suits pages a reader moves through top to bottom.
<%# The preset option rides the root class list: preset: "docs" appends the
app-owned typeset-docs class, a looser rhythm tuned for long-form pages. %>
<%= poetry_typeset(preset: "docs", class: "max-w-2xl") do %>
<h1>Deploying for the first time</h1>
<p>This guide walks through an initial production deploy. Every step is
idempotent, so a run that fails partway is always safe to repeat.</p>
<h2>Before you start</h2>
<p>You will need a provisioned host and push access to the release branch.</p>
<ol>
<li>Prepare the database with <code>bin/rails db:prepare</code>.</li>
<li>Set <code>SECRET_KEY_BASE</code> in the server environment.</li>
<li>Point the load balancer at the freshly booted instances.</li>
</ol>
<p>The <code>docs</code> preset widens the measure and opens up the spacing,
which suits pages a reader moves through top to bottom.</p>
<% end %>
Not typeset
Prose flows around app chrome without restyling it:
And the prose continues below.
<%# Embedded components opt OUT: not-typeset covers the whole subtree. %>
<%= poetry_typeset(class: "max-w-xl") do %>
<p>Prose flows around app chrome without restyling it:</p>
<div class="not-typeset">
<%= poetry_alert do |alert| %>
<% alert.with_title { "Untouched component" } %>
poetry components style themselves — keep them out of the prose cascade.
<% end %>
</div>
<p>And the prose continues below.</p>
<% end %>
Overrides
Field notes
This opening line is nudged larger with a utility while the rest of the prose keeps the typeset rhythm.
Nothing here is special-cased. The heading and the lead above simply carry an extra class, and it takes precedence because the base styles add no specificity of their own.
<%# The base rules are zero-specificity, so a utility class on a single element
wins outright — override just the one thing you want, cascade untouched. %>
<%= poetry_typeset(class: "max-w-xl") do %>
<h1 class="text-primary">Field notes</h1>
<p class="text-lg">This opening line is nudged larger with a utility while
the rest of the prose keeps the typeset rhythm.</p>
<p>Nothing here is special-cased. The heading and the lead above simply carry
an extra class, and it takes precedence because the base styles add no
specificity of their own.</p>
<% end %>
Responsive table
Every plan side by side:
| Plan | Seats | Storage | History | SSO | Support |
|---|---|---|---|---|---|
| Starter | 3 | 5 GB | 30 days | No | Community |
| Team | 25 | 100 GB | 1 year | Yes | |
| Business | Unlimited | 1 TB | Forever | Yes | Priority |
The prose column keeps its narrow rhythm; only the table scrolls.
<%# A wide table would squeeze inside the narrow prose measure. Wrap it in a
typeset-scroll div so the table scrolls sideways while the text stays put. %>
<%= poetry_typeset(class: "max-w-md") do %>
<p>Every plan side by side:</p>
<div class="typeset-scroll">
<table>
<thead>
<tr>
<th>Plan</th><th>Seats</th><th>Storage</th>
<th>History</th><th>SSO</th><th>Support</th>
</tr>
</thead>
<tbody>
<tr><td>Starter</td><td>3</td><td>5 GB</td><td>30 days</td><td>No</td><td>Community</td></tr>
<tr><td>Team</td><td>25</td><td>100 GB</td><td>1 year</td><td>Yes</td><td>Email</td></tr>
<tr><td>Business</td><td>Unlimited</td><td>1 TB</td><td>Forever</td><td>Yes</td><td>Priority</td></tr>
</tbody>
</table>
</div>
<p>The prose column keeps its narrow rhythm; only the table scrolls.</p>
<% end %>
API
Poetry::Ui::Typeset::Component — options are
constructor keywords (the poetry_* helper forwards them);
slots are composed inside the block. Generated from the gem's source documentation.
| Option | Type | Details | Description |
|---|---|---|---|
| preset: | String | Appends typeset-<preset> - a tiny class in the app's own CSS retuning the rhythm variables (e.g. \"docs\"). |
Styling
Every part carries a stable data-slot attribute — target
[data-slot=…] from your own CSS to restyle it. State rides
data attributes on the parts below. This contract is verified against rendered DOM in CI.
| Part | Description |
|---|---|
| [data-slot=typeset] | The prose container - every bare element inside is styled by the app-owned typeset.css; not-typeset (class or data attribute) opts a subtree out |