<x-widget.breadcrumbs>
Breadcrumbs
Where a page sits: a trail of links from the top down to the current page, which screen readers hear as such. On phones, a single link back up instead. Chevron or slash between the steps, an icon for Home, and long labels cut short with the full text on hover.
php artisan larawell:add breadcrumbs
- Also adds
- Icon
Examples
Basic
From the top down; the last is the page you're on. Make the browser window narrow to see the phone version: one link back up.
Show code Hide code
<x-widget.breadcrumbs :items="[
['label' => 'Home', 'href' => '#'],
['label' => 'Orders', 'href' => '#'],
'Order #1042',
]" />
Home icon
iconOnly shows Home as an icon alone, still named "Home" for screen readers. separator="slash" for a lighter look.
Show code Hide code
<x-widget.breadcrumbs separator="slash" :items="[
['label' => 'Home', 'href' => '#', 'icon' => 'home', 'iconOnly' => true],
['label' => 'Settings', 'href' => '#'],
'Billing',
]" />
Long trail
A deep trail wraps onto a second line, and a long label is cut short with the full text in its tooltip.
Show code Hide code
<x-widget.breadcrumbs class="max-w-md" :items="[
['label' => 'Home', 'href' => '#', 'icon' => 'home'],
['label' => 'Projects', 'href' => '#'],
['label' => 'Website redesign for the spring campaign', 'href' => '#'],
['label' => 'Files', 'href' => '#'],
'homepage-hero-final-v3.png',
]" />
Props
Other attributes, such as autocomplete or data-*, are passed through to the element. class styles the component's outer wrapper.
<x-widget.breadcrumbs>
| Prop | Default | Description |
|---|---|---|
| items | Required | The trail, from the top: each a label, or ['label' => 'Orders', 'href' => route('orders.index')], plus 'icon' => 'home' for an icon before it, and 'iconOnly' => true to show the icon alone (the label still names it). The last is the page you're on. |
| label |
'Breadcrumb'
|
The trail's name for screen readers. |
| separator |
'chevron'
|
What sits between the steps: chevron or slash. |
| collapse |
true
|
On phones, only a link back to the page above ("← Orders"), as a whole trail wouldn't fit. The full trail from sm up. |
Accessibility
All 3 examples above are checked with axe-core against the WCAG 2.2 A and AA rules, in the light theme and the dark one, both as the page draws and with each popover, dialog, toast and tooltip opened, on every change. A change that fails can't be merged. Where axe can't decide, such as contrast on SVG text, the test measures the colours itself instead of letting it pass.
Automated checks can't judge everything: how it sounds in a screen reader, and how it feels to use from the keyboard, still need a person. Check those on your own pages too.
Source
What larawell:add breadcrumbs writes to your app with the default namespaces. Prefer to copy by hand? Take these files, plus the ones from
the components it also adds , and the theme and base CSS.
resources/views/components/widget/breadcrumbs/index.blade.php Show
@props([
// The trail, from the top: each a label, or ['label' => 'Orders', 'href' => route('orders.index')], plus
// 'icon' => 'home' for an icon before it, and 'iconOnly' => true to show the icon alone (the label still names it).
// The last is the page you're on.
'items',
// The trail's name for screen readers.
'label' => 'Breadcrumb',
// What sits between the steps: chevron or slash.
'separator' => 'chevron',
// On phones, only a link back to the page above ("← Orders"), as a whole trail wouldn't fit. The full trail
// from sm up.
'collapse' => true,
])
@php
// A typo fails loudly, naming the values that work, instead of quietly rendering something else.
if (! in_array($separator, ['chevron', 'slash'], true)) {
throw new \InvalidArgumentException("Unknown separator [{$separator}] for <x-widget.breadcrumbs>. Use one of: chevron, slash.");
}
$items = array_values(array_map(static fn (string|array $item): array => is_string($item) ? ['label' => $item] : $item, $items));
if ($items === [] || in_array(null, array_map(static fn (array $item): ?string => $item['label'] ?? null, $items), true)) {
throw new \InvalidArgumentException('<x-widget.breadcrumbs> needs items, each with a label: a string, or [\'label\' => …, \'href\' => …].');
}
$last = count($items) - 1;
// The page above, for the phone's back link: the nearest earlier step with a link.
$parent = collect(array_slice($items, 0, $last))->reverse()->first(static fn (array $item): bool => ! empty($item['href']));
$collapsed = $collapse && $parent !== null;
$link = 'hover:text-foreground focus-visible:ring-primary rounded-md underline-offset-4 outline-none hover:underline focus-visible:ring-2';
@endphp
<nav aria-label="{{ $label }}" {{ $attributes->class(['text-sm']) }}>
@if ($collapsed)
{{-- Phones: one link up. The list below is hidden there, so screen readers hear the same thing as the eye. --}}
<a href="{{ $parent['href'] }}" @class(['text-foreground/70 inline-flex max-w-full items-center gap-1 sm:hidden', $link])>
<x-widget.icon name="chevron-left" class="size-4 rtl:rotate-180" />
<span class="truncate">{{ $parent['label'] }}</span>
</a>
@endif
<ol @class(['text-foreground/70 flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-1', 'max-sm:hidden' => $collapsed])>
@foreach ($items as $i => $item)
<li class="flex min-w-0 items-center gap-1.5">
@if ($i > 0)
@if ($separator === 'chevron')
<x-widget.icon name="chevron-right" class="text-foreground/40 size-4 rtl:rotate-180" />
@else
<span aria-hidden="true" class="text-foreground/40">/</span>
@endif
@endif
@php($current = $i === $last)
@php($tag = empty($item['href']) ? 'span' : 'a')
{{-- The current page is named as such (aria-current), and stays a link only if it was given one.
title shows a label cut short in full. --}}
<{{ $tag }}
@if ($tag === 'a') href="{{ $item['href'] }}" @endif
@if ($current) aria-current="page" @endif
title="{{ $item['label'] }}"
@class(['inline-flex min-w-0 items-center gap-1.5', $link => $tag === 'a', 'text-foreground font-medium' => $current])
>
@if (! empty($item['icon']))
<x-widget.icon :name="$item['icon']" class="size-4" />
@endif
{{-- iconOnly: the icon alone shows (a home icon); the label still names it for screen readers. --}}
<span @class(['max-w-48 truncate', 'sr-only' => ! empty($item['icon']) && ! empty($item['iconOnly'])])>{{ $item['label'] }}</span>
</{{ $tag }}>
</li>
@endforeach
</ol>
</nav>