Add a solid status label to the badge

A status label came tonal (the colour's container) or outlined; a label that
has to stand out in the colour itself fell back to the filled count, which is
16px and hidden from screen readers. solid draws the label shape in the
colour's filled pair and is spoken like the others.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
This commit is contained in:
Andreas Reinhold / reini
2026-09-13 21:08:55 +02:00
co-authored by Claude Opus 5
parent ea529bf785
commit e6f4357a2a
4 changed files with 22 additions and 5 deletions
@@ -353,7 +353,7 @@ A value the server changes animates after a morph (the SVG is `wire:ignore`; onl
### `<x-badge>`
- `<x-badge />` — M3's small badge, a dot. `<x-badge value="4" max="99" />` — M3's large badge, a count. Both `error` by default. `floating` pins it to the top-end corner of a `relative` parent: `<span class="relative inline-flex"><x-icon name="mail" /><x-badge value="4" floating /></span>`. A dot or count is `aria-hidden` unless it has a `label`; name the control instead ("Messages, 4 unread").
- `<x-badge value="Expired" tonal />`, `<x-badge value="Active" color="success" tonal />`, `<x-badge value="Pro" outline />` — a status label (not an M3 badge) in the colour's container or a neutral edge. `color` (alias `tone`): `error` default, `primary`, `secondary`, `tertiary`, `success`, `warning`, `info`, `neutral`, `plain`; an unknown colour is `error`.
- `<x-badge value="Expired" tonal />`, `<x-badge value="Active" color="success" tonal />`, `<x-badge value="Built in" color="primary" solid />`, `<x-badge value="Pro" outline />` — a status label (not an M3 badge) in the colour's container, in the colour itself (`solid`, for a label that has to stand out), or a neutral edge. `color` (alias `tone`): `error` default, `primary`, `secondary`, `tertiary`, `success`, `warning`, `info`, `neutral`, `plain`; an unknown colour is `error`.
- `color="neutral"` — neutral ink on every variant: a dot or count in on-surface-variant with surface text, `tonal` in surface-container-high with on-surface-variant text, `outline` in the outline-variant edge with on-surface-variant text.
- `color="plain"` — no background, text or border colour in any variant (shape, size and type stay), so the classes you pass paint it: `<x-badge value="Run" tonal color="plain" class="bg-tertiary-container text-on-tertiary-container" />`. Pass both a background and a text class; an `outline` badge's edge takes the text colour unless you pass a `border-*` colour.
- The value is `value` or the slot; the slot renders as HTML: `<x-badge tonal><x-icon name="bolt" class="size-3" /> Pro</x-badge>`. `value` is escaped. A slot that holds only whitespace or comments is still a dot.
+6 -4
View File
@@ -10,7 +10,8 @@
<span class="relative inline-flex"><x-icon name="notifications" /><x-badge value="4" floating /></span>
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), `outline` in a neutral edge. `color` (alias
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
@@ -31,6 +32,7 @@
'color' => null,
'tone' => null,
'tonal' => false,
'solid' => false,
'outline' => false,
'floating' => false,
'label' => null,
@@ -42,7 +44,7 @@
$markup = $value === null && $slot->hasActualContent();
$text = $value ?? ($markup ? trim((string) $slot) : null);
$dot = blank($text);
$status = ($tonal || $outline) && ! $dot;
$status = ($tonal || $solid || $outline) && ! $dot;
if (! $dot && $max !== null && is_numeric($text) && (int) $text > (int) $max) {
$text = $max.'+';
@@ -61,7 +63,7 @@
];
$paint = match (true) {
$color === 'plain' => '',
! $status => $filled[$color],
! $status, $solid => $filled[$color],
$tonal => $container[$color],
default => 'border-outline-variant text-on-surface-variant',
};
@@ -72,7 +74,7 @@
'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 && ! $dot,
'border' => $outline && ! $tonal && ! $solid && ! $dot,
$paint => $paint !== '',
'absolute top-0.5 end-0.5' => $floating && $dot,
'absolute -top-1 start-[calc(100%-0.75rem)]' => $floating && ! $dot,
@@ -7,6 +7,7 @@
<x-badge value="Expired" tonal />
<x-badge value="Active" color="success" tonal />
<x-badge value="Password" color="info" tonal />
<x-badge value="Built in" color="primary" solid />
<x-badge value="Pro" outline />
<x-badge value="Draft" color="neutral" tonal />
<x-badge value="Archived" color="neutral" outline />
+14
View File
@@ -76,3 +76,17 @@ it('keeps its default colours, and falls back to error on a colour it does not k
->toContain('class="inline-flex shrink-0 items-center justify-center whitespace-nowrap h-6 gap-1 rounded-corner-sm px-2 type-label-md border border-outline-variant text-on-surface-variant"')
->and((string) $this->blade('<x-badge value="3" color="sport-run" />'))->toContain('bg-error text-on-error');
});
it('draws a solid status label in the colour itself, spoken like any label', function () {
$html = (string) $this->blade('<x-badge value="Built in" solid color="primary" />');
expect($html)
->toContain('bg-primary text-on-primary')
->toContain('h-6 gap-1 rounded-corner-sm px-2 type-label-md')
->not->toContain('aria-hidden')
->toContain('>Built in<')
->and((string) $this->blade('<x-badge value="Draft" solid color="neutral" />'))
->toContain('bg-on-surface-variant text-surface')
->and((string) $this->blade('<x-badge solid />'))
->toContain('size-1.5');
});