Skip to main content
Copy for LLM

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

Prose Typography

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.

Prose — readable measure

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::prose> looks exactly as it does anywhere else on the page.

Prose with components inside

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 guide

Every 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:

A prose inside a not-wk-prose block styles its own content

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:

  1. Token-driven — every color, size, radius, and spacing value comes from WireKit's --color-wk-*, --text-wk-*, --padding-wk-*, etc. variables
  2. Auto dark mode — because all values are CSS variables, dark mode works automatically via the .dark class on any ancestor element; no dark: prefixes needed
  3. Themeable — changing your WireKit theme (e.g., switching accent colors or font families) automatically updates all prose typography
  4. 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, and the text is set in a serif.
large Users who scale text up Larger body text and leading grown to match.
Readability presets

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::fonts /> declares it as the bundled family you named in wirekit.fonts.serif, 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.

Prose that reads its own column

In a sidebar

The heading steps down because this column is narrow, not because the window is.

In the main column

The same markup on the same page, at the full-page scale.

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::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-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::personalize():

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',
]);
Tokens, not palette classes

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

Updated in WireKit v2.66.0 (2026-10-13)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store