Skip to content
Brok UI

Loading…

No results

Badge

Open source Core

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.

Version
v1.6.2
Stability
stable
License
MIT
Related
Status
Notification Badge
CI Badge

Preview

Badge
Variant
Size
Tone
previews.components.badge.default.blade.php Blade
<x-ui.badge>{{ __('Badge') }}</x-ui.badge>

Variant options

Default Current
Secondary Current
Destructive Current
Success Current
Warning Current
Info Current
Outline Current
Ghost Current
Soft success Current
Soft warning Current
Soft destructive Current
Soft info Current
Soft neutral Current

Size options

Sm Current
Md Current
Lg Current
Status Current

Tone options

Null Current
Positive Current
Attention Current
Negative Current
Info Current
Neutral Current

Installation

terminal
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.

  • blade resources/views/components/ui/badge.blade.php
  • blade 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.

badge.md
# 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

avatar.blade.php Blade
{{-- `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>
count.blade.php Blade
<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>
group.blade.php Blade
{{-- `<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.blade.php Blade
<div class="w-48 max-w-full">
    <x-ui.badge class="w-full">{{ __('An exceptionally long translated status label') }}</x-ui.badge>
</div>
removable.blade.php Blade
@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>
sizes.blade.php Blade
<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.blade.php Blade
{{-- `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>
tags.blade.php Blade
@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>
trend.blade.php Blade
<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>
variants.blade.php Blade
<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.blade.php Blade
<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

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
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.

badge badge-dot badge-group badge-group-more badge-label badge-leading

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

Compact label and metadata

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
root badge-leading badge-dot badge-label badge-group badge-group-more
Theming hooks
badge

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
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-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, 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 under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Safe

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.

resources/views/components/ui/badge.blade.php Blade
{{--
    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 }}>
resources/views/components/ui/badge/group.blade.php Blade
@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