Prose
A readable long-form content primitive with bounded line length and resilient wrapping for long words and URLs.
Preview
Readable by default
Long-form content stays within a comfortable measure and wraps resiliently, including unusuallylongunbrokencontentthatwouldotherwiseoverflowitscontainer.example.
<x-ui.prose>
<h3 class="text-xl font-semibold">{{ __('Readable by default') }}</h3>
<p class="mt-3 text-muted-foreground">{{ __('Long-form content stays within a comfortable measure and wraps resiliently, including unusuallylongunbrokencontentthatwouldotherwiseoverflowitscontainer.example.') }}</p>
</x-ui.prose>
Installation
php artisan ui:add prose
Registry contract
php artisan ui:add prose
writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.
-
resources/views/components/ui/prose.blade.php
- Registry dependencies
- None — installs on its own.
- Packages
-
composer: jml/brok:^0.2
Use with AI
A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.
# Brok UI: Prose (`prose`)
A readable long-form content primitive with bounded line length and resilient wrapping for long words and URLs.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add prose
```
## Usage
```blade
<x-ui.prose>
<h3 class="text-xl font-semibold">{{ __('Readable by default') }}</h3>
<p class="mt-3 text-muted-foreground">{{ __('Long-form content stays within a comfortable measure and wraps resiliently, including unusuallylongunbrokencontentthatwouldotherwiseoverflowitscontainer.example.') }}</p>
</x-ui.prose>
```
## Props
- `size` (md|sm|lg, default `md`) — Reading measure and base text size: sm (narrower, smaller text), md (default prose width), or lg (wider, larger text).
- `centered` (bool, default `true`) — Centers the bounded-width block horizontally within its parent.
- `as` (string, default `article`) — HTML tag to render: article, div, section, or main.
- `variant` (default|rich, default `default`) — default styles only plain lists in descendant HTML; rich also styles bare descendant headings (h1-h4), paragraphs, task lists, links, inline code, scrollable code blocks, blockquotes, tables, rules and images for rendered Markdown or saved editor HTML.
## Use when
- Use to structure hierarchy, spacing, and responsiveness so content is easier to scan and navigate.
- Rendering long-form text content, such as an article, changelog entry, or rich description, at a readable measure.
- Needing long words and URLs to wrap instead of overflowing their container.
## Avoid when
- Do not let layout primitives substitute for semantics, headings, or interaction rules users still need.
- Laying out short UI copy like labels or button text; prose's bounded width and relaxed leading are for reading passages.
- The content is a structured layout of components rather than flowing text; use stack or grid.
## Anti-patterns
- Using visual layout as a substitute for semantic structure
## Rules
- Use the `<brok:prose>` tag (or `<x-ui.prose>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.
## Links
- Docs: https://brokui.dev/docs/components/prose
- Registry JSON (files, props, contract): https://brokui.dev/r/open/prose.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Examples
{{-- Rich text from the editor or Markdown, with no classes of its own:
headings, paragraphs, links, inline code, a code block, a quote and a
table all take their style from prose. --}}
<x-ui.prose>
<h1>{{ __('Release checklist') }}</h1>
<p>{{ __('Use this page before every deploy. It lives next to the') }} <a href="#runbook">{{ __('incident runbook') }}</a>.</p>
<h2>{{ __('Before the deploy') }}</h2>
<p>{{ __('Run the migrations on a copy of production first, then check the queue with') }} <code>php artisan queue:monitor</code>.</p>
<pre><code>php artisan migrate --pretend
php artisan queue:monitor redis:default --max=100</code></pre>
<blockquote><p>{{ __('Never deploy on a Friday afternoon unless the fix is for an outage.') }}</p></blockquote>
<h3>{{ __('Owners') }}</h3>
<table>
<thead>
<tr><th>{{ __('Step') }}</th><th>{{ __('Owner') }}</th><th>{{ __('When') }}</th></tr>
</thead>
<tbody>
<tr><td>{{ __('Database backup') }}</td><td>{{ __('Operations') }}</td><td>{{ __('One hour before') }}</td></tr>
<tr><td>{{ __('Smoke test') }}</td><td>{{ __('Engineering') }}</td><td>{{ __('Right after') }}</td></tr>
</tbody>
</table>
<hr>
<p>{{ __('Questions go to the #deploys channel.') }}</p>
</x-ui.prose>
<x-ui.prose>
<p>{{ __('Status update for the website relaunch, written in the editor.') }}</p>
<ul>
<li>{{ __('Design review is done') }}</li>
<li>
{{ __('Content migration is in progress') }}
<ul>
<li>{{ __('Blog posts moved') }}</li>
<li>
{{ __('Case studies are waiting for approval') }}
<ul>
<li>{{ __('Two need new images') }}</li>
</ul>
</li>
</ul>
</li>
<li>{{ __('Launch date is not set yet') }}</li>
</ul>
<p>{{ __('Next steps:') }}</p>
<ol>
<li>{{ __('Approve the remaining case studies') }}</li>
<li>
{{ __('Run the accessibility check') }}
<ol>
<li>{{ __('Keyboard') }}</li>
<li>{{ __('Screen reader') }}</li>
</ol>
</li>
<li>{{ __('Agree on a launch date with the client') }}</li>
</ol>
</x-ui.prose>
<x-ui.prose variant="rich">
<h2>{{ __('Release notes') }}</h2>
<p>{{ __('The rich variant styles plain HTML such as saved editor output or a CMS body.') }} <a href="#install">{{ __('Read the install guide') }}</a> {{ __('or run') }} <code>php artisan ui:add prose</code>.</p>
<h3 id="install">{{ __('What changed') }}</h3>
<ul>
<li><strong>{{ __('Lists') }}</strong> {{ __('keep their markers and nest:') }}
<ul>
<li>{{ __('Second level') }}</li>
</ul>
</li>
<li><em>{{ __('Emphasis') }}</em> {{ __('stays readable.') }}</li>
</ul>
<blockquote><p>{{ __('Quotes sit on a start-side rule that mirrors in right-to-left text.') }}</p></blockquote>
<pre><code><x-ui.prose variant="rich">
<h2>Release notes</h2>
</x-ui.prose></code></pre>
<hr>
<table>
<thead><tr><th>{{ __('Element') }}</th><th>{{ __('Behaviour') }}</th></tr></thead>
<tbody>
<tr><td>{{ __('Code block') }}</td><td>{{ __('Scrolls inside itself') }}</td></tr>
<tr><td>{{ __('Table') }}</td><td>{{ __('Wraps its cells') }}</td></tr>
</tbody>
</table>
</x-ui.prose>
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| size | md | sm | lg | md | Reading measure and base text size: sm (narrower, smaller text), md (default prose width), or lg (wider, larger text). |
| centered | bool | true | Centers the bounded-width block horizontally within its parent. |
| as | string | article | HTML tag to render: article, div, section, or main. |
| variant | default | rich | default | default styles only plain lists in descendant HTML; rich also styles bare descendant headings (h1-h4), paragraphs, task lists, links, inline code, scrollable code blocks, blockquotes, tables, rules and images for rendered Markdown or saved editor HTML. |
Slots
default— Long-form content: headings, paragraphs, links, lists, code, quotes and tables, typically HTML from an editor or Markdown.
Renders as article, div, section, main.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- Bounds the block to a reading-friendly max width for the chosen size and applies relaxed line height for long passages.
- Applies text-pretty and break-words so long words and URLs wrap instead of overflowing or creating awkward line breaks.
- Styles plain ul and ol lists in rich text: disc, circle and square bullets by depth, decimal then lower-alpha numbers, a start padding, muted markers, compact item spacing and space around a top-level list. The rules have zero specificity, so a class on a list or item (list-none, ps-0) still wins. variant="rich" styles its lists in ui.css with its other elements instead.
- variant="rich" styles descendant elements through zero-specificity selectors in the components layer (packages/ui ui.css), so a utility class on a descendant still wins; code blocks and wrapped tables scroll inside themselves and a bare table wraps its cells, so the page never scrolls sideways.
- Styles the other blocks of rich text with the same zero specificity: h1 to h4 sized in em from the prose size with semibold weight, paragraph spacing, links in the link colour (an editor mention keeps its chip), inline code and code blocks on the muted surface in the mono font (a wide code block scrolls sideways), a blockquote with a start border in muted text, tables with hairline rows that scroll sideways when wide, horizontal rules and images that fit the column. Neutral tokens only, so dark mode follows.
- Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.
Guidance
Structure content hierarchy and responsive relationships.
Use when
- Use to structure hierarchy, spacing, and responsiveness so content is easier to scan and navigate.
- Rendering long-form text content, such as an article, changelog entry, or rich description, at a readable measure.
- Needing long words and URLs to wrap instead of overflowing their container.
Avoid when
- Do not let layout primitives substitute for semantics, headings, or interaction rules users still need.
- Laying out short UI copy like labels or button text; prose's bounded width and relaxed leading are for reading passages.
- The content is a structured layout of components rather than flowing text; use stack or grid.
Use instead
- Semantic HTML with standard flow
Anti-patterns
- Using visual layout as a substitute for semantic structure
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- No component-owned keyboard interaction; native element behavior applies.
- Focus
none
- Uses semantic foreground text color and does not override child heading levels, so document structure and contrast follow normal content rules.
- The rich variant keeps links underlined with a visible focus ring and never changes heading levels; list markers use the muted foreground token.
- Semantic HTML and a stable
data-slotattribute for styling and scripting hooks. - Focus-visible rings use the
ringtoken, so keyboard focus is always visible. - Disabled and invalid states are conveyed to assistive tech, not by color alone.
- Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
- Labels go through
__()and layout uses logical properties (ms-*,text-start), so it mirrors underdir="rtl"— flip the preview to RTL to confirm. - Dark mode uses the same semantic tokens under the
darkclass; high contrast follows forced-color system tokens.
Livewire
Livewire can update this component through forwarded wire:* attributes.
Source
The exact, editable file ui:add writes
into your app. Previews render this same code; there are no preview-only components.
@props([
'size' => 'md',
'centered' => true,
'as' => 'article',
'variant' => 'default',
])
@php
$styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
$variant = $styles['normalizeVariant']($variant);
$isRich = $variant === 'rich';
$allowedTags = ['article', 'div', 'section', 'main'];
$tag = in_array($as, $allowedTags, true) ? $as : 'article';
$sizes = [
'sm' => 'max-w-xl text-sm',
'md' => 'max-w-prose text-base',
'lg' => 'max-w-3xl text-lg',
];
$sizeKey = array_key_exists($size, $sizes) ? $size : 'md';
// List styles for rich text (editor or Markdown HTML) that has no classes
// of its own. Every rule is wrapped in :where(), so it has no specificity:
// a class on the list or item (`list-none`, `ps-0`) still wins. Nesting is
// matched by depth without overlap (the rules never compete, so their
// order in the stylesheet does not matter): disc, circle, then square
// bullets; decimal, then lower-alpha numbers. A top-level list gets space
// from its siblings only, so a list at the start or end adds no gap.
// variant="rich" skips these and $blocks: it styles every element in
// ui.css instead.
$lists = implode(' ', [
'[:where(&_ul:not(&_li_ul))]:list-disc',
'[:where(&_li_ul:not(&_li_li_ul))]:list-[circle]',
'[:where(&_li_li_ul)]:list-[square]',
'[:where(&_ol:not(&_li_ol))]:list-decimal',
'[:where(&_li_ol)]:list-[lower-alpha]',
'[:where(&_:is(ul,ol))]:ps-6',
'[:where(&_:is(ul,ol):not(&_li_:is(ul,ol)):not(:first-child))]:mt-4',
'[:where(&_:is(ul,ol):not(&_li_:is(ul,ol)):not(:last-child))]:mb-4',
'[:where(&_li_:is(ul,ol))]:my-1',
'[:where(&_li)]:ps-1',
'[:where(&_li+li)]:mt-1',
'[:where(&_li>p)]:my-0',
'marker:text-muted-foreground',
]);
// Block styles for the rest of rich text, with the same zero specificity:
// headings sized in em from the prose size, paragraph rhythm, links (an
// editor mention keeps its chip), inline code and code blocks on the
// muted surface, a quiet blockquote, and tables with hairline rows that
// scroll sideways when wide. Neutral tokens only, so dark mode follows.
$blocks = implode(' ', [
'[:where(&_h1)]:text-[1.5em]',
'[:where(&_h2)]:text-[1.25em]',
'[:where(&_h3)]:text-[1.125em]',
'[:where(&_h4)]:text-[1em]',
'[:where(&_:is(h1,h2,h3,h4))]:font-semibold',
'[:where(&_:is(h1,h2))]:tracking-tight',
'[:where(&_:is(h1,h2,h3,h4))]:leading-snug',
'[:where(&_:is(h1,h2,h3,h4))]:text-foreground',
'[:where(&_:is(h1,h2):not(:first-child))]:mt-8',
'[:where(&_:is(h3,h4):not(:first-child))]:mt-6',
'[:where(&_:is(h1,h2,h3,h4))]:mb-2',
'[:where(&_p:not(&_li_p):not(:first-child))]:mt-3',
'[:where(&_p:not(&_li_p):not(:last-child))]:mb-3',
'[:where(&_:is(blockquote,pre,table):not(:first-child))]:mt-4',
'[:where(&_:is(blockquote,pre,table):not(:last-child))]:mb-4',
'[:where(&_a:not([data-type=mention]))]:text-link',
'[:where(&_a:not([data-type=mention]))]:underline',
'[:where(&_a:not([data-type=mention]))]:underline-offset-2',
'[:where(&_:is(strong,b))]:font-semibold',
'[:where(&_:not(pre)>code)]:rounded-sm',
'[:where(&_:not(pre)>code)]:bg-muted',
'[:where(&_:not(pre)>code)]:px-1',
'[:where(&_:not(pre)>code)]:py-0.5',
'[:where(&_:not(pre)>code)]:font-mono',
'[:where(&_:not(pre)>code)]:text-[0.875em]',
'[:where(&_pre)]:overflow-x-auto',
'[:where(&_pre)]:rounded-md',
'[:where(&_pre)]:border',
'[:where(&_pre)]:border-border',
'[:where(&_pre)]:bg-muted',
'[:where(&_pre)]:px-4',
'[:where(&_pre)]:py-3',
'[:where(&_pre)]:font-mono',
'[:where(&_pre)]:text-[0.875em]',
'[:where(&_pre)]:leading-relaxed',
'[:where(&_pre)]:text-foreground',
'[:where(&_pre_code)]:bg-transparent',
'[:where(&_pre_code)]:p-0',
'[:where(&_blockquote)]:border-s-2',
'[:where(&_blockquote)]:border-border',
'[:where(&_blockquote)]:ps-4',
'[:where(&_blockquote)]:text-muted-foreground',
'[:where(&_table)]:block',
'[:where(&_table)]:max-w-full',
'[:where(&_table)]:overflow-x-auto',
'[:where(&_table)]:border-collapse',
'[:where(&_table)]:text-[0.9375em]',
'[:where(&_th)]:text-start',
'[:where(&_th)]:align-bottom',
'[:where(&_th)]:font-medium',
'[:where(&_:is(th,td))]:border-b',
'[:where(&_:is(th,td))]:border-border',
'[:where(&_:is(th,td))]:py-2',
'[:where(&_:is(th,td))]:pe-4',
'[:where(&_td)]:align-top',
'[:where(&_hr)]:my-6',
'[:where(&_hr)]:border-border',
'[:where(&_img)]:h-auto',
'[:where(&_img)]:max-w-full',
]);
@endphp
<{{ $tag }}
data-slot="prose"
data-size="{{ $sizeKey }}"
@if ($isRich) data-variant="rich" @endif
data-ui-text
{{ $attributes->merge(['class' => trim('w-full min-w-0 break-words text-pretty leading-relaxed text-foreground '.$sizes[$sizeKey].' '.($isRich ? '' : $lists.' '.$blocks).' '.($centered ? 'mx-auto' : ''))]) }}
>{{ $slot }}</{{ $tag }}>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.3.1 - Status
- Stable
- License
-
open - Accessibility reviewed
- No review date recorded
- Last breaking change
- No date recorded
- Deprecation
- Not deprecated
- Contract
-
v6 - Foundation
-
≥ 1.0.0