{{-- A badge: M3's count or dot on an icon, or a short status label.
M3's two badges (BadgeTokens, androidx Compose Material 3, Apache-2.0):
- `` — the small badge, a 6px dot: something new, no number.
- `` — the large badge, 16px tall, label-small: a count. `max` caps what is
shown ("99+").
Both are `error` by default, as M3 draws them, and an `outline` status label draws its edge in
`outline`, not `outline-variant`: M3 asks a badge for 3:1 against what is behind it, and
`outline-variant` is the decorative role dividers use. `floating` pins one to the top-end
corner of a `relative` parent, on M3's anchor geometry — the dot flush in the corner, the
count 2px above it — an icon or an icon button:
The status label is not an M3 badge but every app needs one: `tonal` draws the value in the
colour's container ("Expired" in error-container), `solid` in the colour itself (a label that
has to stand out, "Built in" in primary), `outline` in a neutral edge. `color` (alias
`tone`): `error` (the default), `primary`, `secondary`, `tertiary`, `success`, `warning`, `info`,
and two without a hue of their own:
- `neutral` — neutral ink on every variant: on-surface-variant with surface text as a dot or
count (the ink an outline badge already writes in), surface-container-high with
on-surface-variant text when `tonal`, the outline edge when `outline`.
- `plain` — no background, text or border colour at all, only shape, size and type, so the
caller's classes paint it: ``.
The value is `value`, or the slot, which renders as HTML — an icon beside the word:
` Pro`. With neither it is a dot.
A count or dot says nothing to a screen reader on its own: give the icon's control a label
that includes it ("Notifications, 4 new"), or pass `label` here. --}}
@props([
'value' => null,
'max' => null,
'color' => null,
'tone' => null,
'tonal' => false,
'solid' => false,
'outline' => false,
'floating' => false,
'label' => null,
])
@php
$color = in_array($color ?? $tone, ['primary', 'secondary', 'tertiary', 'error', 'success', 'warning', 'info', 'neutral', 'plain'], true) ? ($color ?? $tone) : 'error';
// hasActualContent(): a slot holding only a comment, or an empty @foreach, is still a dot.
$markup = $value === null && $slot->hasActualContent();
$text = $value ?? ($markup ? trim((string) $slot) : null);
$dot = blank($text);
$status = ($tonal || $solid || $outline) && ! $dot;
if (! $dot && $max !== null && is_numeric($text) && (int) $text > (int) $max) {
$text = $max.'+';
$markup = false;
}
$filled = [
'primary' => 'bg-primary text-on-primary', 'secondary' => 'bg-secondary text-on-secondary', 'tertiary' => 'bg-tertiary text-on-tertiary',
'error' => 'bg-error text-on-error', 'success' => 'bg-success text-on-success', 'warning' => 'bg-warning text-on-warning', 'info' => 'bg-info text-on-info',
'neutral' => 'bg-on-surface-variant text-surface',
];
$container = [
'primary' => 'bg-primary-container text-on-primary-container', 'secondary' => 'bg-secondary-container text-on-secondary-container', 'tertiary' => 'bg-tertiary-container text-on-tertiary-container',
'error' => 'bg-error-container text-on-error-container', 'success' => 'bg-success-container text-on-success-container', 'warning' => 'bg-warning-container text-on-warning-container', 'info' => 'bg-info-container text-on-info-container',
'neutral' => 'bg-surface-container-high text-on-surface-variant',
];
$paint = match (true) {
$color === 'plain' => '',
! $status, $solid => $filled[$color],
$tonal => $container[$color],
default => 'border-outline text-on-surface-variant',
};
$attributes = $attributes
->class([
'inline-flex shrink-0 items-center justify-center whitespace-nowrap',
'size-1.5 rounded-corner-full' => $dot,
'h-4 min-w-4 rounded-corner-full px-1 type-label-sm tabular-nums' => ! $dot && ! $status,
'h-6 gap-1 rounded-corner-sm px-2 type-label-md' => $status,
'border' => $outline && ! $tonal && ! $solid && ! $dot,
$paint => $paint !== '',
'absolute top-0 end-0' => $floating && $dot,
'absolute -top-0.5 start-[calc(100%-0.75rem)]' => $floating && ! $dot,
])
->merge(array_filter([
'aria-label' => $label,
'aria-hidden' => $label === null && ! $status ? 'true' : null,
]));
@endphp
@unless ($dot)@if ($markup){{ $slot }}@else{{ $text }}@endif@endunless