Skip to content
Brok UI

Loading…

No results

Highlight

Open source

Plain text with matched character ranges marked in native <mark> elements, from server offsets or a query; every part is escaped.

Version
v1.0.2
Stability
stable
License
MIT
Related
Item
Command
Record Search

Preview

Atlas launch plan
Draft the launch checklist for Atlas and share it with the client.
Invoice numbering
No word of the query is in this text, so nothing is marked.
previews.components.highlight.default.blade.php Blade
{{-- Search results with their matched ranges marked. The title takes ranges
     that the server search returned; the snippet marks the words of the
     query. Every part is printed escaped. --}}
<div class="flex w-full max-w-md flex-col gap-4">
    <x-ui.item variant="outline">
        <x-ui.item.content>
            <x-ui.item.title>
                <x-ui.highlight :text="__('Atlas launch plan')" :ranges="[[0, 5]]" variant="fill" />
            </x-ui.item.title>
            <x-ui.item.description>
                <x-ui.highlight :text="__('Draft the launch checklist for Atlas and share it with the client.')" :query="__('atlas launch')" />
            </x-ui.item.description>
        </x-ui.item.content>
    </x-ui.item>

    <x-ui.item variant="outline">
        <x-ui.item.content>
            <x-ui.item.title>
                <x-ui.highlight :text="__('Invoice numbering')" :query="__('atlas launch')" variant="fill" />
            </x-ui.item.title>
            <x-ui.item.description>
                <x-ui.highlight :text="__('No word of the query is in this text, so nothing is marked.')" :query="__('atlas launch')" />
            </x-ui.item.description>
        </x-ui.item.content>
    </x-ui.item>
</div>

Installation

terminal
php artisan ui:add highlight

Registry contract

php artisan ui:add highlight 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/highlight.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.

highlight.md
# Brok UI: Highlight (`highlight`)

Plain text with matched character ranges marked in native <mark> elements, from server offsets or a query; every part is escaped.

Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.

## Install

```bash
php artisan ui:add highlight
```

## Usage

```blade
{{-- Search results with their matched ranges marked. The title takes ranges
     that the server search returned; the snippet marks the words of the
     query. Every part is printed escaped. --}}
<div class="flex w-full max-w-md flex-col gap-4">
    <x-ui.item variant="outline">
        <x-ui.item.content>
            <x-ui.item.title>
                <x-ui.highlight :text="__('Atlas launch plan')" :ranges="[[0, 5]]" variant="fill" />
            </x-ui.item.title>
            <x-ui.item.description>
                <x-ui.highlight :text="__('Draft the launch checklist for Atlas and share it with the client.')" :query="__('atlas launch')" />
            </x-ui.item.description>
        </x-ui.item.content>
    </x-ui.item>

    <x-ui.item variant="outline">
        <x-ui.item.content>
            <x-ui.item.title>
                <x-ui.highlight :text="__('Invoice numbering')" :query="__('atlas launch')" variant="fill" />
            </x-ui.item.title>
            <x-ui.item.description>
                <x-ui.highlight :text="__('No word of the query is in this text, so nothing is marked.')" :query="__('atlas launch')" />
            </x-ui.item.description>
        </x-ui.item.content>
    </x-ui.item>
</div>
```

## Props

- `text` (string, default ``) — The plain text to print. It is always escaped, so markup in user data shows as text.
- `ranges` (array, default `[]`) — Matched ranges as [start, end] character offsets (end exclusive) or ['start' => …, 'end' => …]. Overlapping and touching ranges merge; out-of-range offsets are clamped.
- `query` (string|null, default `null`) — Marks every case-insensitive occurrence of each word of the query, in addition to ranges.
- `variant` (weight|fill, default `weight`) — weight turns the match into semibold foreground text and reads on any row background (also an active option); fill adds a quiet muted background for a results page.

## Use when

- Use to summarize, sequence, or present data so users can scan it quickly.
- A search result title or snippet shows which characters matched the query.
- The server returns match offsets, or the typed query is known when the text renders.

## Avoid when

- Do not add display-only ornament when the user needs actionable structure or exact comparison instead.
- The text needs emphasis that is not a match; use strong or em, or prose for a formatted passage.
- The text is rendered in the browser from client data; mark it there with text nodes, never with innerHTML.

## Anti-patterns

- Adding display ornament without informational value

## Rules

- Use the `<brok:highlight>` tag (or `<x-ui.highlight>`) 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/highlight
- Registry JSON (files, props, contract): https://brokui.dev/r/open/highlight.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

Examples

long-content.blade.php Blade
<div class="max-w-xs">
    <p class="text-sm text-muted-foreground">
        <x-ui.highlight
            :text="__('A deliberately long search snippet that wraps over several lines, so the marked words on each line keep their background and weight without clipping: onboarding, onboarding checklist, onboarding.')"
            :query="__('onboarding')"
            variant="fill"
        />
    </p>
</div>

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
text string The plain text to print. It is always escaped, so markup in user data shows as text.
ranges array [] Matched ranges as [start, end] character offsets (end exclusive) or ['start' => …, 'end' => …]. Overlapping and touching ranges merge; out-of-range offsets are clamped.
query string | null null Marks every case-insensitive occurrence of each word of the query, in addition to ranges.
variant weight | fill weight weight turns the match into semibold foreground text and reads on any row background (also an active option); fill adds a quiet muted background for a results page.

Slots

Default Blade slot only.

Data slots

Stable hooks for CSS overrides and browser tests.

highlight highlight-mark

Behavior

  • Offsets count characters (mb_*), not bytes, so accented and multibyte text marks the right letters.
  • Each plain and matched part is printed with {{ }}; the component never outputs raw HTML.
  • With no match the text prints plain, without a mark element.
  • Declares registry capability flags: a11y, authoredStateFixtures, responsive, rtl, darkMode, localized.

Guidance

Scannable data display

Present data for rapid scanning.

Use when

  • Use to summarize, sequence, or present data so users can scan it quickly.
  • A search result title or snippet shows which characters matched the query.
  • The server returns match offsets, or the typed query is known when the text renders.

Avoid when

  • Do not add display-only ornament when the user needs actionable structure or exact comparison instead.
  • The text needs emphasis that is not a match; use strong or em, or prose for a formatted passage.
  • The text is rendered in the browser from client data; mark it there with text nodes, never with innerHTML.

Use instead

  • Table for exact comparison
  • Plain text for a single value

Anti-patterns

  • Adding display ornament without informational value
Anatomy
highlight highlight-mark
Theming hooks
highlight mark (text-foreground, bg-muted in the fill variant)

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 the native mark element; the full text stays one readable string for assistive technology.
  • The match is shown by weight (and a background in the fill variant), not by colour alone.
  • 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/highlight.blade.php Blade
@props([
    // The plain text to print. It is always escaped; markup in it shows as text.
    'text' => '',
    // Matched ranges as [start, end] character offsets (end exclusive), or
    // ['start' => …, 'end' => …]. Offsets count characters, not bytes.
    // Overlapping and touching ranges merge; out-of-range offsets are clamped.
    'ranges' => [],
    // Mark every case-insensitive occurrence of each word of this query
    // instead of (or as well as) `ranges`.
    'query' => null,
    // weight: the match turns bold foreground text (reads on any row,
    // also an active one). fill: a quiet muted background as well.
    'variant' => 'weight',
])

@php
    // Accept a string, a backed enum or a Stringable for `variant`.
    $styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
    $variant = $styles['normalizeVariant']($variant);
    $text = (string) $text;
    $length = mb_strlen($text);
    $spans = [];

    foreach ((array) $ranges as $range) {
        if (! is_array($range)) {
            continue;
        }
        $start = $range['start'] ?? $range[0] ?? null;
        $end = $range['end'] ?? $range[1] ?? null;
        if (! is_numeric($start) || ! is_numeric($end)) {
            continue;
        }
        $spans[] = [max(0, (int) $start), min($length, (int) $end)];
    }

    foreach (preg_split('/\s+/u', trim((string) $query), -1, PREG_SPLIT_NO_EMPTY) ?: [] as $term) {
        $termLength = mb_strlen($term);
        for ($offset = 0; ($position = mb_stripos($text, $term, $offset)) !== false; $offset = $position + $termLength) {
            $spans[] = [$position, min($length, $position + $termLength)];
        }
    }

    $spans = array_values(array_filter($spans, fn (array $span): bool => $span[0] < $span[1]));
    usort($spans, fn (array $a, array $b): int => $a[0] <=> $b[0]);

    $merged = [];
    foreach ($spans as [$start, $end]) {
        $last = array_key_last($merged);
        if ($last !== null && $start <= $merged[$last][1]) {
            $merged[$last][1] = max($merged[$last][1], $end);
        } else {
            $merged[] = [$start, $end];
        }
    }

    $parts = [];
    $cursor = 0;
    foreach ($merged as [$start, $end]) {
        if ($start > $cursor) {
            $parts[] = ['text' => mb_substr($text, $cursor, $start - $cursor), 'match' => false];
        }
        $parts[] = ['text' => mb_substr($text, $start, $end - $start), 'match' => true];
        $cursor = $end;
    }
    if ($cursor < $length) {
        $parts[] = ['text' => mb_substr($text, $cursor), 'match' => false];
    }

    $markClass = $variant === 'fill'
        ? 'rounded-xs bg-muted font-medium text-foreground box-decoration-clone'
        : 'bg-transparent font-semibold text-foreground';
@endphp

{{-- Highlight: plain text with its matched ranges in native <mark> elements.
     Every part is printed escaped, so a title or snippet from user data can
     never inject markup. The newline after @endif keeps the two directives
     apart (PHP drops it after the closing tag, so no space is printed). --}}
<span data-slot="highlight" data-variant="{{ $variant === 'fill' ? 'fill' : 'weight' }}" {{ $attributes }}>@foreach ($parts as $part)@if ($part['match'])<mark data-slot="highlight-mark" class="{{ $markClass }}">{{ $part['text'] }}</mark>@else{{ $part['text'] }}@endif
@endforeach</span>

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.0.2
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