Record Search
A debounced search box that returns grouped hits in a popover, each group labelled by which field matched.
Preview
Matches order references, customer names, postcodes, and invoice numbers.
<x-ui.record-search
value="90142"
placeholder="{{ __('Search orders, customers, or invoices…') }}"
hint="{{ __('Matches order references, customer names, postcodes, and invoice numbers.') }}"
:groups="[
[
'label' => __('Orders'),
'hits' => [
['id' => '#90142', 'hint' => __('Awaiting payment, placed 2 days ago'), 'href' => '#'],
['id' => '#90143', 'hint' => __('Shipped with PostNL'), 'href' => '#'],
],
],
[
'label' => __('Customers'),
'hits' => [
['id' => 'Anna de Vries', 'hint' => __('3 orders, Rotterdam'), 'href' => '#', 'mono' => false],
],
],
]"
/>
Installation
php artisan ui:add record-search
Note
This component ships an Alpine behavior module at
resources/js/ui/record-search.js. Import it once from your bundle so it registers on alpine:init:
import './record-search.js';
Registry contract
php artisan ui:add record-search
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/record-search.blade.php
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: Record Search (`record-search`)
A debounced search box that returns grouped hits in a popover, each group labelled by which field matched.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add record-search
```
## Usage
```blade
<x-ui.record-search
value="90142"
placeholder="{{ __('Search orders, customers, or invoices…') }}"
hint="{{ __('Matches order references, customer names, postcodes, and invoice numbers.') }}"
:groups="[
[
'label' => __('Orders'),
'hits' => [
['id' => '#90142', 'hint' => __('Awaiting payment, placed 2 days ago'), 'href' => '#'],
['id' => '#90143', 'hint' => __('Shipped with PostNL'), 'href' => '#'],
],
],
[
'label' => __('Customers'),
'hits' => [
['id' => 'Anna de Vries', 'hint' => __('3 orders, Rotterdam'), 'href' => '#', 'mono' => false],
],
],
]"
/>
```
## Props
- `groups` (array, default `[]`) — Grouped hits to render in the popover: each entry is ['label','hits' => [['id','hint','href','mono']]]. A hit may carry any other keys (for example title); primaryField picks the one shown as the main line.
- `placeholder` (string|null, default `null`) — Input placeholder and accessible name. Defaults to a localized "Search…".
- `wireModel` (string|null, default `null`) — Livewire property to bind. A consumer-supplied wire:model* directive on the tag always wins over this compatibility prop.
- `debounce` (int, default `400`) — Milliseconds to wait after the last keystroke before dispatching record-search:query (and the generated wire:model.live.debounce).
- `value` (string, default ``) — Initial query value.
- `hint` (string|null, default `null`) — Screen-reader-only hint under the box, e.g. naming which fields are searched.
- `client` (bool, default `false`) — Emit an event instead of navigating when a hit is chosen. The query event fires in both modes.
- `event` (string, default `record-search:select`) — Event a client-driven hit dispatches. Named by the caller so the component does not impose its vocabulary on a host that already has one.
- `primaryField` (string, default `id`) — Set as primary-field. The hit key shown as the main line. id (default) renders exactly as before. Any other key (for example title) leads the row in the body font, truncated, with the hint under it and the id as small mono secondary text at the end; a hit without that key falls back to its id. mono => true on a hit sets the main line in mono.
## Use when
- Use to orient users and help them move across pages, sections, or commands.
- Searching by any of several identifiers (reference, customer, line item, …) where the field that matched needs to stay visible.
- A live query needs its own debounced popover of server- or Livewire-provided results.
## Avoid when
- Do not hide primary wayfinding in novelty interactions or deep nested structures if straightforward navigation would be clearer.
- The full result set is already loaded client-side — use <x-ui.command> to filter it instead of round-tripping a query.
- A single plain debounced input is enough and there is no grouped-results popover — use <x-ui.input-group> with a search addon, an input carrying wire:model and <x-ui.input-group.clear />.
## Anti-patterns
- Hiding primary wayfinding in novelty interactions
## Rules
- Use the `<brok:record-search>` tag (or `<x-ui.record-search>`) 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/record-search
- Registry JSON (files, props, contract): https://brokui.dev/r/open/record-search.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Examples
{{-- Client-state mode: choosing a hit is an event, not a navigation. Type to
open the popover — the results are authored here, not fetched. --}}
<div
x-data="{ last: null }"
x-on:record-search:select="last = `select ${$event.detail.id} from ${$event.detail.group}`"
class="flex flex-col gap-3"
>
<x-ui.record-search
client
:groups="[
['label' => __('Order reference'), 'hits' => [
['id' => '#1042', 'hint' => __('Rotterdam, PostNL')],
['id' => '#1043', 'hint' => __('Amsterdam, DHL')],
]],
['label' => __('Postcode'), 'hits' => [
['id' => '3011 AA', 'hint' => __('4 orders')],
]],
]"
:hint="__('Matches an order reference, a postcode, or an invoice number.')"
/>
<p class="text-xs text-muted-foreground">
{{ __('Host received:') }}
<span data-slot="preview-event-log" class="font-mono text-foreground" x-text="last ?? '—'"></span>
</p>
</div>
{{-- Slot-driven, client-state form: `groups` never exists as a PHP prop here
— a preloaded dataset filtered entirely in Alpine drives the popover's
grouped hits, with no server round trip. Wired through the already
-documented `record-search:query` event (the same one a Livewire/fetch
host would listen for), not a cross-scope read of the input's own Alpine
state. Shown next to the ordinary array-prop form so both are visible
side by side. This component has no aria-activedescendant/arrow-key
navigation in either form today (see the component doc comment) — the
slot doesn't add or remove that contract. --}}
<div class="flex flex-col gap-6">
<div>
<p class="mb-1.5 text-xs font-medium text-muted-foreground">{{ __('Server-array form (props)') }}</p>
<x-ui.record-search
placeholder="{{ __('Search orders, customers, or invoices…') }}"
:groups="[
[
'label' => __('Orders'),
'hits' => [
['id' => '#90142', 'hint' => __('Awaiting payment, placed 2 days ago'), 'href' => '#'],
],
],
]"
/>
</div>
<div
x-data="{
records: [
{ id: '#90142', hint: @js(__('Awaiting payment, placed 2 days ago')), group: @js(__('Orders')) },
{ id: '#90143', hint: @js(__('Shipped with PostNL')), group: @js(__('Orders')) },
{ id: 'Anna de Vries', mono: false, hint: @js(__('3 orders, Rotterdam')), group: @js(__('Customers')) },
],
hitGroups: [],
search(query) {
const q = query.trim().toLowerCase();
if (q === '') {
this.hitGroups = [];
return;
}
const matches = this.records.filter((r) => r.id.toLowerCase().includes(q));
const byGroup = {};
for (const hit of matches) {
(byGroup[hit.group] ??= []).push(hit);
}
this.hitGroups = Object.entries(byGroup).map(([label, hits]) => ({ label, hits }));
},
}"
x-on:record-search:query="search($event.detail.query)"
>
<p class="mb-1.5 text-xs font-medium text-muted-foreground">{{ __('Slot-driven form (client state)') }}</p>
<x-ui.record-search :debounce="0" placeholder="{{ __('Search a preloaded list…') }}">
<x-slot:results>
<template x-if="hitGroups.length === 0">
<p data-slot="record-search-empty" class="px-2 py-3 text-center text-xs text-muted-foreground">
{{ __('No matches yet — keep typing.') }}
</p>
</template>
<template x-for="group in hitGroups" :key="group.label">
<div>
<div class="px-2 pt-2 pb-1">
<div class="text-xs font-medium tracking-wide text-muted-foreground uppercase" x-text="group.label"></div>
</div>
<template x-for="hit in group.hits" :key="hit.id">
<a
href="#"
role="option"
aria-selected="false"
class="flex min-h-10 w-full items-baseline gap-2 rounded px-2 py-1.5 text-start transition-colors hover:bg-accent focus-visible:bg-accent focus-visible:outline-none"
>
<span class="text-sm font-medium text-popover-foreground" :class="{ 'font-mono': hit.mono !== false }" x-text="hit.id"></span>
<span class="min-w-0 truncate text-xs text-muted-foreground" x-text="hit.hint"></span>
</a>
</template>
</div>
</template>
</x-slot:results>
</x-ui.record-search>
</div>
</div>
Long Content
<div class="max-w-xs">
<x-ui.record-search
value="rotterdam"
:groups="[
[
'label' => __('Customers'),
'hits' => [
[
'id' => 'Anna de Vries',
'mono' => false,
'hint' => __('A deliberately long hint that verifies truncation instead of layout overflow'),
'href' => '#',
],
],
],
]"
/>
</div>
Primary Field
{{-- primary-field: lead each hit with its title; the id moves to small secondary text. The box is full width up to max-w-sm, so it also fits the narrow column below. --}}
@php
$groups = [
[
'label' => __('Documents'),
'hits' => [
['id' => 'DOC-2291', 'title' => __('Quarterly revenue review for the northern region'), 'hint' => __('Updated 2 days ago'), 'href' => '#'],
['id' => 'DOC-2305', 'title' => __('Onboarding checklist'), 'hint' => __('Shared with Operations'), 'href' => '#'],
],
],
[
'label' => __('People'),
'hits' => [
['id' => 'USR-18', 'title' => 'Anna de Vries', 'hint' => __('Finance, Rotterdam'), 'href' => '#'],
],
],
];
@endphp
<div class="flex w-full flex-col gap-6">
<x-ui.record-search value="review" primary-field="title" :groups="$groups" />
<div class="w-64 max-w-full">
<x-ui.record-search value="review" primary-field="title" :groups="$groups" />
</div>
</div>
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| groups | array | [] | Grouped hits to render in the popover: each entry is ['label','hits' => [['id','hint','href','mono']]]. A hit may carry any other keys (for example title); primaryField picks the one shown as the main line. |
| placeholder | string | null | null | Input placeholder and accessible name. Defaults to a localized "Search…". |
| wireModel | string | null | null | Livewire property to bind. A consumer-supplied wire:model* directive on the tag always wins over this compatibility prop. |
| debounce | int | 400 | Milliseconds to wait after the last keystroke before dispatching record-search:query (and the generated wire:model.live.debounce). |
| value | string | Initial query value. | |
| hint | string | null | null | Screen-reader-only hint under the box, e.g. naming which fields are searched. |
| client | bool | false | Emit an event instead of navigating when a hit is chosen. The query event fires in both modes. |
| event | string | record-search:select | Event a client-driven hit dispatches. Named by the caller so the component does not impose its vocabulary on a host that already has one. |
| primaryField | string | id | Set as primary-field. The hit key shown as the main line. id (default) renders exactly as before. Any other key (for example title) leads the row in the body font, truncated, with the hint under it and the id as small mono secondary text at the end; a hit without that key falls back to its id. mono => true on a hit sets the main line in mono. |
Slots
results— Replaces the grouped-hits markup inside the role="listbox" popover (the command list), for hits that live in client (Alpine) state rather than the groups prop — a preloaded dataset filtered locally, or a fetch response stashed in a store. Renders inside the same popover chrome (positioning, border, scroll). Unset, the popover renders exactly as before, straight from groups, including the quiet empty-state message; when set, the caller owns the empty state too (reuse the record-search-empty data-slot and class recipe for visual parity). Slot-rendered rows are not command items, so the arrow keys skip them and Enter on a focused slot link is left to the browser; render <x-ui.command.item> rows in the slot to get the keyboard model over a client-rendered list.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- Debounces input by the configured delay before dispatching record-search:query with the current text.
- When wireModel is set (and no consumer wire:model* directive is present), generates a wire:model.live.debounce directive at the same delay.
- Escape and an outside click close the results popover; the popover only opens once the box has focus and reopens are driven by the host re-rendering groups.
- The box and the popover are the command parts in field mode (<x-ui.command field :filter="false">): ArrowUp/Down and Home/End move aria-activedescendant over the hits and Enter activates the active one, while every server-returned hit stays visible.
- Renders a quiet empty-state message when groups is empty so the popover is never blank.
- By default each hit is a plain link, so a match can be opened in a new tab and works with no JavaScript.
- In client mode each hit becomes a button that dispatches record-search:select with the hit and the label of the group it matched in.
- results is an additive slot for hits that live in client (Alpine) code rather than the groups prop; unset, the component renders exactly as before.
- The box is w-full up to max-w-sm, so it fits a narrow container or a phone screen; widen it on a wide screen with an important class such as class="!max-w-lg". The results popover is at least 20rem wide but never wider than the viewport minus 2rem, so it stays on screen.
- Installs a JavaScript behavior module when the registry item includes resources/js/ui files.
- Declares registry capability flags: a11y, interactive, authoredStateFixtures, responsive, rtl, darkMode, localized.
Guidance
Orient users and move between destinations.
Use when
- Use to orient users and help them move across pages, sections, or commands.
- Searching by any of several identifiers (reference, customer, line item, …) where the field that matched needs to stay visible.
- A live query needs its own debounced popover of server- or Livewire-provided results.
Avoid when
- Do not hide primary wayfinding in novelty interactions or deep nested structures if straightforward navigation would be clearer.
- The full result set is already loaded client-side — use <x-ui.command> to filter it instead of round-tripping a query.
- A single plain debounced input is enough and there is no grouped-results popover — use <x-ui.input-group> with a search addon, an input carrying wire:model and <x-ui.input-group.clear />.
Use instead
- Visible links and local navigation
Anti-patterns
- Hiding primary wayfinding in novelty interactions
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- Escape
- Focus
managed
- Meet the WCAG 2.2 AA target declared in meta.a11y.
- The input is the command combobox (role="combobox", aria-expanded bound to the popover, aria-activedescendant pointing at the active hit) and the results popover is a role="listbox" of role="option" hits under labelled role="group" headings.
- 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
Add a stable wire:key when Livewire can reorder this interactive component.
<div wire:key="record-search-{{ $record->id }}">
<x-ui.record-search
value="90142"
placeholder="{{ __('Search orders, customers, or invoices…') }}"
hint="{{ __('Matches order references, customer names, postcodes, and invoice numbers.') }}"
:groups="[
[
'label' => __('Orders'),
'hits' => [
['id' => '#90142', 'hint' => __('Awaiting payment, placed 2 days ago'), 'href' => '#'],
['id' => '#90143', 'hint' => __('Shipped with PostNL'), 'href' => '#'],
],
],
[
'label' => __('Customers'),
'hits' => [
['id' => 'Anna de Vries', 'hint' => __('3 orders, Rotterdam'), 'href' => '#', 'mono' => false],
],
],
]"
/>
</div>
Source
The exact, editable file ui:add writes
into your app. Previews render this same code; there are no preview-only components.
{{--
Record Search — one debounced search box across several identifiers, with
hits GROUPED by which one matched.
Grouping is the point: pasting an order reference, a postcode, and an
invoice number can all return "a result", but they mean different things,
and an operator needs to see which field the match came from before they
trust it. Different from a bare <x-ui.command> (a palette that filters a
preloaded list client-side) and a plain <x-ui.input-group> search (a
debounced input with no results popover) — this is a debounced query with
a grouped-results popover the host renders from a live search.
The box and the popover are the command item's parts in field mode
(`<x-ui.command field :filter="false">`): `command.input` is the combobox,
`command.list` the listbox, `command.group` a labelled group and
`command.item` a hit. That is where the keyboard model comes from —
ArrowUp/Down, Home/End move `aria-activedescendant` over the hits and Enter
activates the active one — while `filter=false` keeps every server-returned
hit visible (the query matched on fields that are not in the row text).
This file owns the field chrome (icon, clear control, hint), the debounced
query event, the popover placement and the grouped hit markup.
No fetch layer is assumed: the debounced query is exposed two ways so it
works under Livewire or plain JavaScript — a `wire:model.live.debounce`
directive (the same one a plain search input carries) when `wireModel` is
set, and a `record-search:query` event on every debounced keystroke either
way. The host re-renders `groups` with the new results (a Livewire property
update, or a page fragment swap); this component only owns the box and
the popover chrome.
Choosing a hit has the same two state models the rest of the toolbar has.
By default each hit is a plain link (`href`), so a match can be opened in
a new tab and works with no JavaScript. Pass `client` when the list owns
its own state — a hit becomes a button that dispatches
`record-search:select` with the hit and the group it matched in, which is
the only way this works in a preview with no server to navigate to.
`groups` is a PHP value snapshotted once at render time — fine when a
Livewire property update or a page fragment swap re-renders it, useless
when the hits already live in Alpine state on the client (a preloaded
dataset filtered locally, or a fetch response stashed in a store) and
change with no re-render to hang them on. So the grouped-hits markup
takes a `results` slot instead of an Alpine-expression prop: it replaces
everything inside the listbox popover, and renders inside the same popover
chrome, so a consumer's own `x-for`/`x-text` updates the list live. The
input, debounce, popover positioning, and the empty-state token recipe
(`data-slot="record-search-empty"`) stay reusable either way; a
slot-driven empty state has to be authored by the consumer, because the
server has no way to see a client array's length at render time — reuse
the same `data-slot`/class recipe for visual parity. Leave `results` unset
and the component renders exactly as before, straight from `groups`.
Slot-rendered rows are not command items (they never register), so the
arrow keys skip them; a consumer who needs the keyboard model over a
client-rendered list renders `<x-ui.command.item>` rows in the slot. Enter
on a focused slot link is left to the browser.
Generalised from `orders/search.blade.php`. The domain identifier list
(Mollie id, postcode, invoice number, …) is left out — `hint` accepts
whatever sentence the host wants for the search box's screen-reader hint.
--}}
@props([
// [['label' => 'Orders', 'hits' => [['id' => '#1042', 'hint' => '…', 'href' => '#', 'mono' => true]]]]
// A hit's `id` is set in mono (a reference, a code); `'mono' => false` for
// a name or title.
'groups' => [],
'placeholder' => null,
// Livewire property to bind, e.g. "query". Compatibility path — a
// consumer-supplied wire:model* directive on the tag always wins.
'wireModel' => null,
'debounce' => 400,
'value' => '',
// Screen-reader-only hint under the box, e.g. listing which fields match.
'hint' => null,
// Emit `record-search:select` instead of navigating, for a host that owns
// its own state. The query event fires in both modes.
'client' => false,
// The event a client-driven hit dispatches. Named by the caller, because a
// component that hardcodes its event name forces every host to adopt our
// vocabulary -- the same objection that made the chip items take a per-entry
// verb rather than a mode flag.
'event' => 'record-search:select',
// The hit key shown as the main line. `id` (default) keeps the reference
// as the headline; name another key (e.g. 'title') to lead with it, and
// the id moves to a small secondary line at the end of the row.
'primaryField' => 'id',
])
@php
$groups = array_values(array_filter((array) $groups, 'is_array'));
$client = (bool) $client;
$placeholder ??= __('Search…');
$delay = max(0, (int) $debounce);
// Per instance: two record-searches on one page (the live preview) must
// not share the hint's id.
$hintId = 'record-search-hint-'.\Illuminate\Support\Str::random(6);
$primaryField = is_string($primaryField) && $primaryField !== '' ? $primaryField : 'id';
$text = static fn (mixed $value): string => is_scalar($value) || $value instanceof \Stringable ? (string) $value : '';
// A consumer-supplied wire:model* directive on the tag wins; otherwise
// `wireModel` generates the debounced one. Both reach the input through
// the one `{{ $attributes->… }}` bag a component tag accepts.
$generatedWireModel = [];
if ($attributes->whereStartsWith('wire:model')->getAttributes() === [] && filled($wireModel)) {
$generatedWireModel = ["wire:model.live.debounce.{$delay}ms" => $wireModel];
}
@endphp
<div
data-slot="record-search"
x-data="{ open: false }"
x-on:click.outside="open = false"
{{ $attributes->whereDoesntStartWith('wire:model')->merge(['class' => 'relative w-full max-w-sm']) }}
>
{{-- The command root owns `query` and the keyboard model. Escape closes
the popover first and keeps the text (capture, so the palette's own
Escape — which clears the query — only runs once the popover is
already closed; preventDefault stops the search input's native
clear-on-Escape). --}}
<x-ui.command
field
:filter="false"
:reset-on-select="false"
x-init="query = {{ \Illuminate\Support\Js::from((string) $value) }}"
x-on:keydown.escape.capture="if (open && query.length > 0) { $event.preventDefault(); $event.stopPropagation(); open = false; $refs.input.focus(); }"
>
{{-- The debounced query event listens on this wrapper (input events
bubble), because a Blade component tag cannot carry a `{{ }}`
expression inside an attribute name. --}}
<div class="relative" x-on:input.debounce.{{ $delay }}ms="$dispatch('record-search:query', { query: query })">
<x-ui.command.input
type="search"
data-slot="record-search-input"
x-ref="input"
expanded="open && query.length > 0"
x-on:focus="open = true"
:placeholder="$placeholder"
:aria-label="$placeholder"
:aria-describedby="$hint ? $hintId : null"
{{ $attributes->whereStartsWith('wire:model')->merge($generatedWireModel) }}
/>
{{-- Designed clear control (the native cancel glyph is hidden by ui.css). --}}
<span class="absolute inset-y-0 end-0 flex items-center pe-2" x-show="query.length > 0" x-cloak>
<button
type="button"
data-slot="record-search-clear"
x-on:click="query = ''; reset(); open = true; $refs.input.focus(); $dispatch('record-search:query', { query: '' })"
aria-label="{{ __('Clear search') }}"
class="inline-flex size-6 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring motion-reduce:transition-none"
>
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12" /></svg>
</button>
</span>
</div>
@if ($hint)
<p id="{{ $hintId }}" class="sr-only">{{ $hint }}</p>
@endif
<div
data-slot="record-search-results"
x-show="open && query.length > 0"
x-cloak
class="absolute start-0 top-full z-popover mt-1 w-full min-w-[min(20rem,calc(100vw-2rem))] max-w-[calc(100vw-2rem)] rounded-md border border-border bg-popover shadow-md"
>
<x-ui.command.list aria-label="{{ $placeholder }}">
@isset($results)
{{ $results }}
@else
@forelse ($groups as $group)
<x-ui.command.group :heading="$group['label'] ?? null" :separator="! $loop->first">
@foreach ((array) ($group['hits'] ?? []) as $hit)
@php
// primaryField: lead with that key (falling back to
// the id when a hit lacks it); the id then shows as
// secondary text unless it is the same string.
$primaryText = $text($hit[$primaryField] ?? null);
$primaryText = $primaryText !== '' ? $primaryText : $text($hit['id'] ?? null);
$secondaryId = $text($hit['id'] ?? null);
// Unique per hit across groups: the same reference
// can match in two groups and each row needs its
// own option id for aria-activedescendant.
$key = ($group['label'] ?? '').'/'.($secondaryId !== '' ? $secondaryId : $primaryText);
$secondaryId = $secondaryId === $primaryText ? '' : $secondaryId;
@endphp
@if ($client)
<x-ui.command.item
:value="$key"
x-on:click="$dispatch({{ Illuminate\Support\Js::from($event) }}, {{ Illuminate\Support\Js::from($hit + ['group' => $group['label'] ?? '']) }})"
class="min-h-10"
>
@if ($primaryField === 'id')
<span @class(['font-medium', 'font-mono' => $hit['mono'] ?? true])>{{ $hit['id'] ?? '' }}</span>
<span class="min-w-0 truncate text-xs text-muted-foreground">{{ $hit['hint'] ?? '' }}</span>
@else
<span class="flex min-w-0 flex-1 flex-col">
<span data-slot="record-search-hit-primary" @class(['truncate font-medium', 'font-mono' => $hit['mono'] ?? false])>{{ $primaryText }}</span>
@if ($text($hit['hint'] ?? null) !== '')
<span class="truncate text-xs text-muted-foreground">{{ $hit['hint'] }}</span>
@endif
</span>
@if ($secondaryId !== '')
<span data-slot="record-search-hit-id" class="shrink-0 font-mono text-xs text-muted-foreground">{{ $secondaryId }}</span>
@endif
@endif
</x-ui.command.item>
@else
<x-ui.command.item :value="$key" :href="$hit['href'] ?? '#'" class="min-h-10">
@if ($primaryField === 'id')
<span @class(['font-medium', 'font-mono' => $hit['mono'] ?? true])>{{ $hit['id'] ?? '' }}</span>
<span class="min-w-0 truncate text-xs text-muted-foreground">{{ $hit['hint'] ?? '' }}</span>
@else
<span class="flex min-w-0 flex-1 flex-col">
<span data-slot="record-search-hit-primary" @class(['truncate font-medium', 'font-mono' => $hit['mono'] ?? false])>{{ $primaryText }}</span>
@if ($text($hit['hint'] ?? null) !== '')
<span class="truncate text-xs text-muted-foreground">{{ $hit['hint'] }}</span>
@endif
</span>
@if ($secondaryId !== '')
<span data-slot="record-search-hit-id" class="shrink-0 font-mono text-xs text-muted-foreground">{{ $secondaryId }}</span>
@endif
@endif
</x-ui.command.item>
@endif
@endforeach
</x-ui.command.group>
@empty
<p data-slot="record-search-empty" class="px-2 py-3 text-center text-xs text-muted-foreground">
{{ __('No matches yet — keep typing.') }}
</p>
@endforelse
@endisset
</x-ui.command.list>
</div>
</x-ui.command>
</div>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.5.0 - 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