Badge
Compact status label with semantic variants, sizes, and a read-model `tone` (positive/attention/negative/info/neutral) that resolves to the soft recipe in one place.
Preview
<x-ui.badge>{{ __('Badge') }}</x-ui.badge>
Variant options
Size options
Tone options
Installation
php artisan ui:add badge
Registry contract
php artisan ui:add badge
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/badge.blade.php -
resources/views/components/ui/badge/group.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.
# Brok UI: Badge (`badge`)
Compact status label with semantic variants, sizes, and a read-model `tone` (positive/attention/negative/info/neutral) that resolves to the soft recipe in one place.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add badge
```
## Usage
```blade
<x-ui.badge>{{ __('Badge') }}</x-ui.badge>
```
## Props
- `variant` (default|secondary|destructive|success|warning|info|outline|ghost, default `default`) — Visual recipe: default, secondary, destructive, success, warning, info, outline, ghost, or a soft-* tone recipe.
- `size` (md|sm|lg, default `md`) — Badge height and padding: sm, md, or lg.
- `as` (string, default `span`) — Tag rendered: span, button, or a; any other value falls back to span.
- `tone` (mixed|null, default `null`) — Semantic state (success, warning, destructive, info, neutral) that resolves to the matching soft variant, overriding variant when set.
- `dot` (bool, default `false`) — Adds a leading status dot: true uses the badge's text color, or pass a tone name for a colored dot on a neutral badge.
- `leading` (mixed|null, default `null`) — Medallion, such as an icon or avatar, rendered at the start edge and sized to the badge.
- `items` (array, default `[]`) — Declared by @props in the registry Blade source.
- `max` (int, default `3`) — Declared by @props in the registry Blade source.
## Use when
- Use for compact labels, counts, and concise metadata that support scanning.
- Labeling a status, count, category, or trend inline next to other content.
- Marking a semantic state with the tone prop so the same word always resolves to the same color across the app.
## Avoid when
- Do not force badges or short labels to carry complex explanations that need sentence-level context.
- The label is the trigger for a primary action; use button instead of as="button".
## Anti-patterns
- Encoding complex meaning in a badge alone
## Rules
- Use the `<brok:badge>` tag (or `<x-ui.badge>`) 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/badge
- Registry JSON (files, props, contract): https://brokui.dev/r/open/badge.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Examples
{{-- `leading` slot: a medallion hugging the start edge, sized to the badge. --}}
<div class="flex flex-wrap items-center gap-2">
<x-ui.badge variant="secondary">
<x-slot:leading><x-ui.avatar size="xs" fallback="SR" class="!bg-primary !text-primary-foreground" /></x-slot:leading>
{{ __('Sofia Rodrigues') }}
</x-ui.badge>
<x-ui.badge variant="outline">
<x-slot:leading><x-ui.avatar size="xs" fallback="JD" /></x-slot:leading>
{{ __('Jane Doe') }}
</x-ui.badge>
</div>
<div class="flex flex-wrap items-center gap-4">
{{-- Count pills: rounded-full + tabular-nums keep multi-digit counts aligned --}}
<x-ui.badge variant="secondary" class="rounded-full tabular-nums">{{ __('8') }}</x-ui.badge>
<x-ui.badge variant="destructive" class="rounded-full tabular-nums">{{ __('99+') }}</x-ui.badge>
<x-ui.badge variant="default" class="rounded-full tabular-nums">{{ __('32,499+') }}</x-ui.badge>
{{-- Inline notification count anchored to a label --}}
<span class="inline-flex items-center gap-2 text-sm text-foreground">
{{ __('Inbox') }}
<x-ui.badge variant="destructive" size="sm" class="rounded-full tabular-nums">{{ __('12') }}</x-ui.badge>
</span>
</div>
{{-- `<x-ui.badge.group>` shows the first `max` labels and folds the rest into "+N"
(the hidden labels live in its title and sr-only text). --}}
<div class="flex flex-col items-start gap-3">
<x-ui.badge.group :items="['React', 'Vue', 'Svelte', 'Angular', 'Solid']" :max="3" />
<x-ui.badge.group :items="['Finance', 'Warehouse', 'Marketing', 'Support']" :max="2" tone="info" size="sm" />
</div>
Long Content
<div class="w-48 max-w-full">
<x-ui.badge class="w-full">{{ __('An exceptionally long translated status label') }}</x-ui.badge>
</div>
Removable
@php
$tags = [__('Design'), __('Engineering'), __('Marketing'), __('Sales')];
@endphp
<div class="flex flex-wrap items-center gap-2">
@foreach ($tags as $tag)
{{-- Trailing close button uses me-1 negative inset + logical ms for RTL safety --}}
<x-ui.badge variant="secondary" class="ps-2 pe-1">
{{ $tag }}
<button
type="button"
class="ms-0.5 inline-flex size-3.5 items-center justify-center rounded-full text-secondary-foreground/70 transition-colors hover:bg-secondary-foreground/10 hover:text-secondary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
aria-label="{{ __('Remove :tag', ['tag' => $tag]) }}"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" class="size-2.5" aria-hidden="true">
<path d="M3 3 11 11M11 3 3 11" />
</svg>
</button>
</x-ui.badge>
@endforeach
</div>
<div class="flex flex-wrap items-center gap-2">
<x-ui.badge size="sm">{{ __('Small') }}</x-ui.badge>
<x-ui.badge size="md">{{ __('Default') }}</x-ui.badge>
<x-ui.badge size="lg">{{ __('Large') }}</x-ui.badge>
</div>
Status Dots
{{-- `dot`: a leading status dot, in a tone on a neutral badge or in the badge's own colour. --}}
<div class="flex flex-wrap items-center gap-2">
<x-ui.badge variant="outline" dot="success">{{ __('Online') }}</x-ui.badge>
<x-ui.badge variant="outline" dot="warning">{{ __('Away') }}</x-ui.badge>
<x-ui.badge variant="outline" dot="destructive">{{ __('Busy') }}</x-ui.badge>
<x-ui.badge variant="outline" dot="neutral">{{ __('Offline') }}</x-ui.badge>
<x-ui.badge tone="positive" dot>{{ __('Deployed') }}</x-ui.badge>
</div>
@php
$tags = ['react', 'typescript', 'tailwind', 'next.js', 'shadcn/ui', 'motion', 'radix'];
@endphp
<div class="flex flex-wrap items-center gap-1.5">
@foreach ($tags as $tag)
<x-ui.badge variant="ghost" size="sm" class="font-normal text-muted-foreground">{{ $tag }}</x-ui.badge>
@endforeach
</div>
<div class="flex flex-wrap items-center gap-2">
{{-- Upward delta --}}
<x-ui.badge variant="success" class="rounded-full tabular-nums">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-3" aria-hidden="true">
<path fill-rule="evenodd" d="M10 17a.75.75 0 0 1-.75-.75V5.612L5.29 9.77a.75.75 0 0 1-1.08-1.04l5.25-5.5a.75.75 0 0 1 1.08 0l5.25 5.5a.75.75 0 1 1-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0 1 10 17Z" clip-rule="evenodd" />
</svg>
<span class="sr-only">{{ __('Up') }}</span>
{{ __('12.5%') }}
</x-ui.badge>
{{-- Downward delta --}}
<x-ui.badge variant="destructive" class="rounded-full tabular-nums">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-3" aria-hidden="true">
<path fill-rule="evenodd" d="M10 3a.75.75 0 0 1 .75.75v10.638l3.96-4.158a.75.75 0 1 1 1.08 1.04l-5.25 5.5a.75.75 0 0 1-1.08 0l-5.25-5.5a.75.75 0 1 1 1.08-1.04l3.96 4.158V3.75A.75.75 0 0 1 10 3Z" clip-rule="evenodd" />
</svg>
<span class="sr-only">{{ __('Down') }}</span>
{{ __('3.2%') }}
</x-ui.badge>
{{-- Flat / no change --}}
<x-ui.badge variant="outline" class="rounded-full tabular-nums">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="size-3" aria-hidden="true">
<path d="M4 10h12" />
</svg>
<span class="sr-only">{{ __('No change') }}</span>
{{ __('0.0%') }}
</x-ui.badge>
</div>
<div class="flex flex-wrap items-center gap-2">
<x-ui.badge>{{ __('Default') }}</x-ui.badge>
<x-ui.badge variant="secondary">{{ __('Secondary') }}</x-ui.badge>
<x-ui.badge variant="destructive">{{ __('Destructive') }}</x-ui.badge>
<x-ui.badge variant="success">{{ __('Success') }}</x-ui.badge>
<x-ui.badge variant="warning">{{ __('Warning') }}</x-ui.badge>
<x-ui.badge variant="info">{{ __('Info') }}</x-ui.badge>
<x-ui.badge variant="outline">{{ __('Outline') }}</x-ui.badge>
<x-ui.badge variant="ghost">{{ __('Ghost') }}</x-ui.badge>
</div>
With Icon
<div class="flex flex-wrap items-center gap-2">
<x-ui.badge variant="success">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-3" aria-hidden="true">
<path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 0 1 .143 1.052l-8 10.5a.75.75 0 0 1-1.127.075l-4.5-4.5a.75.75 0 0 1 1.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 0 1 1.05-.143Z" clip-rule="evenodd" />
</svg>
{{ __('Verified') }}
</x-ui.badge>
<x-ui.badge variant="info">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-3" aria-hidden="true">
<path fill-rule="evenodd" d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-7-4a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM9 9a.75.75 0 0 0 0 1.5h.253a.25.25 0 0 1 .244.304l-.459 2.066A1.75 1.75 0 0 0 10.747 15H11a.75.75 0 0 0 0-1.5h-.253a.25.25 0 0 1-.244-.304l.459-2.066A1.75 1.75 0 0 0 9.253 9H9Z" clip-rule="evenodd" />
</svg>
{{ __('Info') }}
</x-ui.badge>
{{-- Icon-only badge: aria-label provides the accessible name --}}
<x-ui.badge variant="secondary" aria-label="{{ __('Featured') }}">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-3" aria-hidden="true">
<path fill-rule="evenodd" d="M10.868 2.884c-.321-.772-1.415-.772-1.736 0l-1.83 4.401-4.753.381c-.833.067-1.171 1.107-.536 1.651l3.62 3.102-1.106 4.637c-.194.813.691 1.456 1.405 1.02L10 15.591l4.069 2.485c.713.436 1.598-.207 1.404-1.02l-1.106-4.637 3.62-3.102c.635-.544.297-1.584-.536-1.65l-4.752-.382-1.831-4.401Z" clip-rule="evenodd" />
</svg>
</x-ui.badge>
</div>
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | default | secondary | destructive | success | warning | info | outline | ghost | default | Visual recipe: default, secondary, destructive, success, warning, info, outline, ghost, or a soft-* tone recipe. |
| size | md | sm | lg | md | Badge height and padding: sm, md, or lg. |
| as | string | span | Tag rendered: span, button, or a; any other value falls back to span. |
| tone | mixed | null | null | Semantic state (success, warning, destructive, info, neutral) that resolves to the matching soft variant, overriding variant when set. |
| dot | bool | false | Adds a leading status dot: true uses the badge's text color, or pass a tone name for a colored dot on a neutral badge. |
| leading | mixed | null | null | Medallion, such as an icon or avatar, rendered at the start edge and sized to the badge. |
| items | array | [] | Declared by @props in the registry Blade source. |
| max | int | 3 | Declared by @props in the registry Blade source. |
Slots
default—x-ui.badge.group— Installed subcomponent from the registry item.
Renders as span, button, a.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- tone overrides variant when both are given, resolving to the soft recipe: a tinted background with readable text.
- as renders span, button, or a; any other value falls back to span so a caller cannot inject an arbitrary tag.
- The leading slot renders a medallion, such as an icon or avatar, hugging the start edge and sized to the badge.
- Inline display primitive with no JavaScript behavior.
- Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.
Guidance
Communicate concise status, count, or metadata.
Use when
- Use for compact labels, counts, and concise metadata that support scanning.
- Labeling a status, count, category, or trend inline next to other content.
- Marking a semantic state with the tone prop so the same word always resolves to the same color across the app.
Avoid when
- Do not force badges or short labels to carry complex explanations that need sentence-level context.
- The label is the trigger for a primary action; use button instead of as="button".
Use instead
- Visible explanatory text
Anti-patterns
- Encoding complex meaning in a badge alone
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- No component-owned keyboard interaction; native element behavior applies.
- Focus
none
- When as="button", the badge renders a real button element with type="button", so it stays reachable by keyboard and is announced as interactive.
- 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
Livewire can update this component through forwarded wire:* attributes.
Source
The exact, editable files ui:add writes
into your app. Previews render this same code; there are no preview-only components.
{{--
Badge.
`as` lets the badge be the thing you press without growing a second
component that has to be kept the same shape by hand. A count you can open
is still a badge; it just needs to be a button to be reachable by keyboard
and announced as one.
`tone` is the semantic entry point a read model uses: it sends
positive/attention/negative/info/neutral and never a CSS class or a
variant name, so the palette lives in one place and a status pill, a
sidebar count and a KPI delta all resolve the same word to the same
colour. A tone always maps to the soft recipe (tinted ground, readable
text); pass `variant` instead when a solid fill is wanted. When both are
given, `tone` wins, because it is the more specific statement.
--}}
{{--
`dot`: a leading status dot — true takes the badge's text colour, or a tone
(success | warning | destructive | info | neutral) for a coloured dot on a
neutral badge. `leading` slot: a medallion (an avatar, an icon) hugging the
start edge, sized to the badge whatever size the child asked for.
--}}
@props([
'variant' => 'default',
'size' => 'md',
'as' => 'span',
'tone' => null,
'dot' => false,
'leading' => null,
])
@php
$styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
$variant = $styles['normalizeVariant']($variant);
$badge = $styles['badge'];
if ($tone !== null) {
$variant = $badge['tones'][$tone] ?? $badge['tones']['neutral'];
}
// Anything else would let a caller inject an arbitrary tag name.
$tag = in_array($as, ['span', 'button', 'a'], true) ? $as : 'span';
$dotClass = match (true) {
$dot === false || $dot === null || $dot === 'false' => null,
in_array($dot, ['success', 'warning', 'destructive', 'info'], true) => 'bg-'.$dot,
$dot === 'neutral' => 'bg-muted-foreground',
default => 'bg-current',
};
$hasLeading = $leading !== null && ! $leading->isEmpty();
$classes = trim(
$badge['base']
.' '.($badge['variants'][$variant] ?? $badge['variants']['default'])
.' '.($badge['sizes'][$size] ?? $badge['sizes']['md'])
.($tag === 'span' ? '' : ' cursor-pointer transition-colors')
);
@endphp
<{{ $tag }}
data-slot="badge"
data-variant="{{ $variant }}"
@if ($tone !== null) data-tone="{{ $tone }}" @endif
data-size="{{ $size }}"
@if ($tag === 'button') type="button" @endif
{{ $attributes->merge(['class' => $classes]) }}
>
@if ($hasLeading)
<span data-slot="badge-leading" class="-ms-1 inline-flex shrink-0 items-center [&>*]:!size-4 [&>*]:!text-2xs" aria-hidden="true">{{ $leading }}</span>
@endif
@if ($dotClass)
<span data-slot="badge-dot" class="size-2 shrink-0 rounded-full {{ $dotClass }}" aria-hidden="true"></span>
@endif
{{-- Icons and controls inside the label render inline: the preflight makes
svg a block element, which would break a badge onto two lines; a bare
span (a hand-made dot) gets its box back the same way. --}}
<span data-slot="badge-label" class="min-w-0 truncate [&_svg]:inline-block [&_svg]:shrink-0 [&_svg]:align-[-0.125em] [&>span]:inline-block [&>span]:align-[-0.125em] [&_button]:align-middle">{{ $slot }}</span>
</{{ $tag }}>
@props([
// The labels to show; anything past `max` folds into a "+N" badge.
'items' => [],
'max' => 3,
'variant' => 'secondary',
'size' => 'md',
// Optional tone for every item badge (a read model's positive/attention/…).
'tone' => null,
])
@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);
$items = array_values(array_filter((array) $items, static fn ($item) => is_scalar($item) && (string) $item !== ''));
$max = max(0, (int) $max);
$visible = array_slice($items, 0, $max);
$overflow = count($items) - count($visible);
$hidden = array_slice($items, $max);
@endphp
{{-- A row of badges that folds the rest into "+N" — the "+N" carries the
hidden labels in its title and an sr-only list, so nothing is lost. --}}
<span
data-slot="badge-group"
{{ $attributes->merge(['class' => 'inline-flex flex-wrap items-center gap-2']) }}
>
@foreach ($visible as $item)
<x-ui.badge :variant="$variant" :size="$size" :tone="$tone">{{ $item }}</x-ui.badge>
@endforeach
@if ($overflow > 0)
<x-ui.badge data-slot="badge-group-more" variant="outline" :size="$size" :title="implode(', ', $hidden)" class="tabular-nums">
<span aria-hidden="true">+{{ $overflow }}</span>
<span class="sr-only">{{ trans_choice(':count more: :items|:count more: :items', $overflow, ['count' => $overflow, 'items' => implode(', ', $hidden)]) }}</span>
</x-ui.badge>
@endif
@if ($items === [])
{{ $slot }}
@endif
</span>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.6.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