Prose
The <x-wirekit:: component is a typography wrapper that styles raw HTML content (headings, paragraphs, lists, blockquotes, code blocks, tables, links, images, and horizontal rules) using WireKit design tokens. It serves a similar purpose to @tailwindcss/typography but is fully driven by WireKit's CSS variable system, ensuring consistent theming across light and dark modes.
Usage
Getting Started with WireKit
WireKit is a free, MIT-licensed UI component library for Laravel Livewire. It provides dozens of accessible, themeable components out of the box.
"The best component library is one that gets out of your way."
- Design token driven
- WCAG 2.2 AA accessible
- Dark mode automatic
composer require pushery/wirekit
Rendering Markdown
Prose is ideal for wrapping rendered Markdown (e.g., from Str::markdown() or a CMS):
<x-wirekit::prose>
{!! Str::markdown($article->body) !!}
</x-wirekit::prose>
Size Variants
The size prop adjusts the base font size while keeping all other proportions relative.
{{-- Small prose — compact text --}}
<x-wirekit::prose size="sm">
<p>Smaller body text for captions or footnotes.</p>
</x-wirekit::prose>
{{-- Default (medium) --}}
<x-wirekit::prose>
<p>Standard body text for articles and documentation.</p>
</x-wirekit::prose>
{{-- Large prose — expanded reading --}}
<x-wirekit::prose size="lg">
<p>Larger body text for landing pages or hero sections.</p>
</x-wirekit::prose>
Reading Measure
Long-form text reads best at a constrained line length. prose clamps to a readable ~65-character measure by default (the --measure-wk token), so body copy stays comfortable on wide screens instead of stretching edge to edge. Pass measure="wide" (~78ch) for a roomier column, or measure="none" to fill the full container width.
This paragraph is capped at a readable line length by default, so long-form text stays comfortable to read on a wide screen instead of running the full width of its container. Set measure="none" to opt out and fill the container, or measure="wide" for a slightly roomier column.
Blog Post Example
<article>
<x-wirekit::prose size="lg">
<h1>{{ $post->title }}</h1>
<p>Published on {{ $post->published_at->format('F j, Y') }}</p>
{!! $post->rendered_html !!}
</x-wirekit::prose>
</article>
Size Reference
| Size | CSS Variable | Typical Use |
|---|---|---|
sm |
--text-wk-sm |
Footnotes, captions, sidebars |
md |
--text-wk-md |
Articles, documentation (default) |
lg |
--text-wk-lg |
Landing pages, hero sections |
Styled Elements
The prose wrapper applies design-token-driven styles to the following HTML elements using Tailwind's descendant selector syntax ([&_element]). None of these rules reach an element a WireKit component renders; see Components Inside Prose.
| Element | Styles Applied |
|---|---|
h1 |
2XL font size, heading weight, tight leading, bottom margin |
h2 |
XL font size, heading weight, tight leading, top + bottom margin |
h3 |
LG font size, heading weight, tight leading, top + bottom margin |
h4 |
MD font size, heading weight, top + bottom margin |
p |
Bottom margin |
a |
Accent color, underline with 2px offset |
strong |
Heading font weight |
ul |
Disc markers, left padding, bottom margin |
ol |
Decimal markers, left padding, bottom margin |
li |
Bottom margin (xs) |
blockquote |
Left border (4px), left padding, italic, muted text, bottom margin |
code |
Mono font, smaller size, muted background, inline padding, rounded |
pre |
Muted background, rounded, padded, bottom margin; long lines wrap unless the pre carries tabindex |
pre code |
Transparent background, no padding (inherits from pre) |
table |
Full width, collapsed borders, bottom margin |
th |
Left-aligned, heading weight, padding, double bottom border |
td |
Padding, subtle bottom border |
hr |
Border color from tokens, vertical margin (xl) |
img |
Rounded corners, vertical margin |
Components Inside Prose
Prose styles the HTML you give it, not the WireKit components you place in it. A code block, a button link or a table inside <x-wirekit:: looks exactly as it does anywhere else on the page.
Publish the assets
Run the command once after installing and again after every upgrade. The integration guide explains when you can skip it.
php artisan vendor:publish --tag=wirekit-assets
The code block above and the button below keep their own spacing, color and radius.
Read the integration guideEvery element a WireKit component renders carries data-wk-prose-skip, and every prose rule leaves such an element out. The attribute exempts that one element and nothing below it: whatever you pass into a component's slot is still styled by prose, so prose placed inside a table cell styles its own paragraphs and links.
Opting Out Markup of Your Own
Your own markup carries no marker. To keep a block of it out of prose's reach, add not-wk-prose to its wrapper. The wrapper and everything inside it keep their own styling:
<x-wirekit::prose>
{!! Str::markdown($article->body) !!}
{{-- Nothing in this block takes prose's spacing, list markers or link styles. --}}
<div class="not-wk-prose">
@include('partials.newsletter-signup')
</div>
</x-wirekit::prose>
The opt-out covers the raw markup below the wrapper. A prose nested in the block starts over and styles its own content, at its own size and density, so a page written in prose can show an example that is a prose itself:
Reading the example
The page around the example is prose. The block below opts out of it.
- This list is raw markup in the block, so the page's prose leaves it alone.
The example
This prose sits inside the block and styles its own heading and paragraph, at its own compact density.
This holds for one more level too: a not-wk-prose block inside that nested prose leaves it again. For a component of your own that should behave like a WireKit one, put data-wk-prose-skip on each element the component styles itself. Prose leaves those elements alone and still styles the content passed in:
{{-- resources/views/components/callout.blade.php --}}
<aside class="callout">
<p data-wk-prose-skip class="callout-title">{{ $title }}</p>
{{ $slot }}
</aside>
Why Prose Instead of @tailwindcss/typography?
The @tailwindcss/typography plugin is excellent, but it uses its own hardcoded color palette. WireKit's Prose component differs in important ways:
- Token-driven — every color, size, radius, and spacing value comes from WireKit's
--color-wk-*,--text-wk-*,--padding-wk-*, etc. variables - Auto dark mode — because all values are CSS variables, dark mode works automatically via the
.darkclass on any ancestor element; nodark:prefixes needed - Themeable — changing your WireKit theme (e.g., switching accent colors or font families) automatically updates all prose typography
- Personalizable — use
WireKit::to override prose styles at the class levelpersonalize()
Readability presets
preset is a separate axis from size / density / measure — it tunes
readability and composes with them rather than overriding them.
Each preset moves size and leading together, on purpose: bumping font-size alone makes large text read worse, not better.
| Preset | For | What it does |
|---|---|---|
chat |
A message bubble | Tighter leading. Adds no measure clamp — the bubble already caps line length, so a clamp would only fight it. |
reading |
Long-form articles | Roomier leading for sustained reading, and the text is set in a serif. |
large |
Users who scale text up | Larger body text and leading grown to match. |
Chat preset — tight leading, sized for a message bubble that already caps its own line length.
Reading preset — roomier leading for long-form text you sit with for a while.
Large preset — bigger body text, with the leading grown to match so it actually reads easier.
{{-- 1. A chat bubble body — the bubble caps the width, prose sets the rhythm --}}
<x-wirekit::prose preset="chat" size="sm">
{!! $markdownHtml !!}
</x-wirekit::prose>
Where the Reading Serif Comes From
preset="reading" sets the text in --font-wk-serif, and that token belongs to the font system
rather than to a web font this component fetches on its own.
<x-wirekit:: declares it as the bundled family you named in
wirekit., and as the reader's own platform serif when you named none. An application
that bundles Lora renders a reading page in Lora; one that bundles nothing renders it in the system
serif — and neither makes a request to anybody else.
The measure follows the face. The clamp counts 65 characters of the serif's own ch metric, which
every bundled serif ships, instead of the sans metric that would otherwise size a column of text
nobody is reading in sans.
Want the roomier leading without the serif? Set the family back in your own stylesheet, which cascades on top of the shipped one:
/* app.css — the reading rhythm, in your sans */
.wk-prose[data-preset='reading'] {
font-family: var(--font-wk-sans);
}
Adapting to the Column
A viewport breakpoint cannot see a column. Prose in an 18rem sidebar on a desktop screen is wide by
every media query in the sheet, so it keeps the scale built for a full page: an <h2> at
--text-wk-xl with the full --padding-wk-y-xl of air above it, in a column narrower than the
heading.
container makes the wrapper a size container and tightens the scale below 30rem of its own
width — the compact rhythm, without the caller having to know which column the prose landed in.
On a phone the preview scrolls sideways. Its main column keeps at least 30rem, the width below which
container tightens the scale, so the two columns stay a comparison instead of becoming two narrow ones.
Why it is opt-in. container-type: inline-size makes an element's inline size independent of
its contents. Prose that is laid out by its parent — a grid track, a flex item with a basis, a block
in a column — is unaffected, and that is most prose. Prose whose width comes from its own contents —
inside an inline-block, a max-content track, a floated box — collapses. The prop is where you
say which one you have.
Streaming
Prose is stable while text streams in: appending paragraphs, headings, lists or code blocks never moves the text the reader is already on. The block rhythm uses bottom-margins, which cannot reflow earlier siblings.
That means a wire:stream of rendered markdown needs no special mode:
{{-- 1. Blocks append as they arrive; nothing above them shifts --}}
<x-wirekit::prose preset="chat">
<span wire:stream="answer">{!! $answerHtml !!}</span>
</x-wirekit::prose>
Typographic polish
Where the browser supports it, headings are balanced (text-wrap: balance) and
paragraphs avoid short last lines (text-wrap: pretty). This is progressive
enhancement: it is feature-detected, purely additive, and browsers without it
wrap normally — there is no shim and nothing depends on it.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
size |
string |
'md' |
Base font size: 'sm', 'md', 'lg' |
variant |
string |
'default' |
Text tone: 'default', 'muted' |
density |
string |
'comfortable' |
Heading and paragraph spacing rhythm: 'comfortable' (long-form article), 'compact' (tighter, for marketing pages) |
measure |
string |
'default' |
Readable line-length clamp: 'default' (~65ch), 'wide' (~78ch), 'none' (full container width) |
preset |
string|null |
null |
Readability tuning on top of size/density/measure: 'chat' (tight leading), 'reading' (roomier leading, set in the serif), 'large' (bigger text and leading) |
container |
bool |
false |
Adapt the rhythm to the prose's own column width instead of the viewport — see Adapting to the Column. |
scope |
string|null |
null |
Scoped personalization key |
Keyboard Interaction
This component is a layout wrapper. Keyboard interaction is delegated to its children.
Code blocks wrap; they do not scroll
A <pre> inside <x-wirekit:: wraps long lines instead of scrolling horizontally.
That is deliberate: a horizontal scroll region has to be reachable by keyboard, and Prose
styles markup it did not author — it cannot put a tab stop on your <pre>. A scrolling
block with no tab stop hides the rest of the line from anyone who is not using a pointer.
If you want horizontal scrolling, author the keyboard model yourself and Prose will honor it:
{{-- 1. tabindex makes the block a tab stop, so the hidden part of the line is reachable. --}}
{{-- 2. role + aria-label tell a screen reader what the region is. --}}
{{-- 3. Prose sees the tabindex and switches this block back to horizontal scrolling. --}}
<x-wirekit::prose>
<pre tabindex="0" role="region" aria-label="Example configuration"><code>{{ $longLine }}</code></pre>
</x-wirekit::prose>
For code you control, prefer <x-wirekit:: — it ships the
scroll region already wired, with a copy button and language labeling.
Design Tokens
| Token | Used for |
|---|---|
--font-wk-sans |
Prose body font family |
--font-wk-mono |
Inline <code> + <pre> font family |
--font-wk-heading-weight |
Heading weight |
--text-wk-sm / --text-wk-md / --text-wk-lg / --text-wk-xl |
Body / lead / heading font sizes |
--leading-wk-relaxed |
Body line height |
--leading-wk-tight |
Heading line height |
--color-wk-text |
Body text |
--color-wk-text-muted |
Blockquote text |
--color-wk-accent-text |
Inline <a> color |
--color-wk-bg-muted |
Inline <code> + <pre> background |
--color-wk-border / --color-wk-border-subtle |
Blockquote left bar + table cell border |
--radius-wk-sm / --radius-wk-md |
Inline <code> + <pre> border radius |
--padding-wk-x-sm / --padding-wk-x-md / --padding-wk-x-lg |
Block / inline padding |
--padding-wk-y-xs / --padding-wk-y-sm / --padding-wk-y-md / --padding-wk-y-lg / --padding-wk-y-xl |
Vertical rhythm between blocks |
Personalization
Override classes globally via WireKit:::
use Pushery\WireKit\WireKit;
WireKit::personalize('prose', [
'base' => 'font-[family-name:var(--font-wk-sans)] text-[color:var(--color-wk-text)] leading-relaxed'
.' [&_a]:text-[color:var(--color-wk-accent-content)] [&_a]:underline',
]);
Palette classes such as text-gray-900 and text-blue-600 work, and both silently opt the
component out of the theme: a gray-900 body stays near-black when the reader switches to dark
mode, and a blue-600 link stays blue under every preset — including the ones whose whole point
is a different accent. Nothing reports it, because nothing is wrong: the classes do exactly what
they say.
personalize() replaces the component's classes, so whatever it hands over is the complete
answer. That makes it the one place where reaching for a palette class costs the most, and the
one example a reader is most likely to copy.
Usage & Conventions
Prop conventions — this component uses one or more of the shared semantic prop names (
intent/variant/tone/surface). See Prop naming conventions for the canonical vocabulary, alias matrix, and decision tree.
Further Reading
- @tailwindcss/typography — the Tailwind plugin that inspired this component's purpose
- MDN: CSS child combinator — how the
[&_element]selector works in Tailwind v4