Skip to content
Brok UI

Loading…

No results

Prose

Open source

A readable long-form content primitive with bounded line length and resilient wrapping for long words and URLs.

Version
v1.3.1
Stability
stable
License
MIT
Related
Stack
Grid
Surface

Preview

Size
previews.components.prose.default.blade.php 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>
Sm Current
Md Current
Lg Current

Installation

terminal
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.

  • blade 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.

prose.md
# 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

blocks.blade.php Blade
{{-- 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>
lists.blade.php Blade
<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>
rich.blade.php Blade
<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>&lt;x-ui.prose variant="rich"&gt;
    &lt;h2&gt;Release notes&lt;/h2&gt;
&lt;/x-ui.prose&gt;</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

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
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.

prose

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

Layout and content structure

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
root
Theming hooks
prose

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
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-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, 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 under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Safe

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.

resources/views/components/ui/prose.blade.php Blade
@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