Prose
The <x-wirekit::prose> 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.1 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 child selector syntax ([&_element]):
| 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, horizontal scroll |
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 |
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::personalize()to override prose styles at the class level
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. |
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>
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
get the normal wrapping they always had — 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), 'large' (bigger text and leading) |
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::prose> 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::code-block> — 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 |
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::personalize():
use Pushery\WireKit\WireKit;
WireKit::personalize('prose', [
'base' => 'font-sans text-gray-900 leading-relaxed [&_a]:text-blue-600 [&_a]:underline',
]);
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