Charts
Token-themed data visualisations — the core multi-series chart plus dependency-free SVG variants for sankey, word-cloud, venn and violin plots.
Preview
- Revenue
- $8,750.00
- Expense
- $1,852.00
- Revenue
- Costs
- Profit
- p50
- p95
- p99
- Blade
- 62%
- JavaScript
- 21%
- CSS
- 11%
<div class="flex flex-col items-center gap-6">
{{-- The panel-header headline: a marker + label row, then a large value
with its delta badge — the key above a "Total revenue" chart. Expense
is a dashed series with tone="positive" on its delta, so a falling
expense still reads as good news even though the arrow points down. --}}
<x-ui.chart-legend size="lg" :items="[
['label' => __('Revenue'), 'color' => 'chart-1', 'value' => '$8,750.00', 'delta' => 12],
['label' => __('Expense'), 'color' => 'chart-2', 'dashed' => true, 'value' => '$1,852.00', 'delta' => -3, 'tone' => 'positive'],
]" marker="line" />
<x-ui.chart-legend :items="[['label' => __('Revenue')], ['label' => __('Costs')], ['label' => __('Profit')]]" />
<x-ui.chart-legend marker="line" :items="[['label' => 'p50', 'color' => 'chart-1'], ['label' => 'p95', 'color' => 'chart-2'], ['label' => 'p99', 'color' => 'chart-3']]" />
<x-ui.chart-legend orientation="vertical" marker="square" :items="[
['label' => __('Blade'), 'value' => '62%', 'color' => 'chart-1'],
['label' => __('JavaScript'), 'value' => '21%', 'color' => 'chart-2'],
['label' => __('CSS'), 'value' => '11%', 'color' => 'chart-3'],
]" />
</div>
Installation
php artisan ui:add chart-legend
Registry contract
php artisan ui:add chart-legend
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/chart-legend.blade.php
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: Chart Legend (`chart-legend`)
A static legend for any chart: swatch, label and optional value per series, as a wrapping row or a stack, with circle, square or line markers, an optional delta badge, and a large lg headline size.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add chart-legend
```
## Usage
```blade
<div class="flex flex-col items-center gap-6">
{{-- The panel-header headline: a marker + label row, then a large value
with its delta badge — the key above a "Total revenue" chart. Expense
is a dashed series with tone="positive" on its delta, so a falling
expense still reads as good news even though the arrow points down. --}}
<x-ui.chart-legend size="lg" :items="[
['label' => __('Revenue'), 'color' => 'chart-1', 'value' => '$8,750.00', 'delta' => 12],
['label' => __('Expense'), 'color' => 'chart-2', 'dashed' => true, 'value' => '$1,852.00', 'delta' => -3, 'tone' => 'positive'],
]" marker="line" />
<x-ui.chart-legend :items="[['label' => __('Revenue')], ['label' => __('Costs')], ['label' => __('Profit')]]" />
<x-ui.chart-legend marker="line" :items="[['label' => 'p50', 'color' => 'chart-1'], ['label' => 'p95', 'color' => 'chart-2'], ['label' => 'p99', 'color' => 'chart-3']]" />
<x-ui.chart-legend orientation="vertical" marker="square" :items="[
['label' => __('Blade'), 'value' => '62%', 'color' => 'chart-1'],
['label' => __('JavaScript'), 'value' => '21%', 'color' => 'chart-2'],
['label' => __('CSS'), 'value' => '11%', 'color' => 'chart-3'],
]" />
</div>
```
## Props
- `items` (array, default `[]`) — Rows to render: each an object with label, an optional color (a chart-N token or class), an optional value, an optional delta (number or preformatted string), an optional tone override (positive|negative|neutral) for the delta badge, and an optional dashed flag (line marker only).
- `orientation` (string, default `horizontal`) — Lays rows out as a wrapping row or a vertical stack.
- `marker` (string, default `circle`) — Swatch shape: circle, square, or line (a short dash, for line charts). A series can request a dashed stroke on the line marker with its own dashed flag.
- `label` (string, default `Legend`) — Accessible name for the legend region.
- `size` (default|lg, default `default`) — default renders the compact swatch/label/value row. lg renders the panel-header headline: a marker + label row, then the value in a large tabular-number style with its delta badge beside it.
## Use when
- Use when trend, distribution, flow, intensity, or relationship questions are easier to answer visually than in raw numbers.
- Labelling series colours for an SVG chart, or for an ApexCharts chart rendered with its built-in legend turned off.
- Needing the legend values to read as label/value pairs, such as beside a pie or donut chart.
## Avoid when
- Do not use a chart when users mainly need exact lookup or when a single metric would be clearer as text or a stat.
- The chart already renders ApexCharts' own legend; do not add a second one.
- Only a single tooltip is needed at the pointer, not a static key; use chart-tooltip instead.
## Anti-patterns
- Decorative charts without a meaningful question
## Rules
- Use the `<brok:chart-legend>` tag (or `<x-ui.chart-legend>`) 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/chart-legend
- Registry JSON (files, props, contract): https://brokui.dev/r/open/chart-legend.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| items | array | [] | Rows to render: each an object with label, an optional color (a chart-N token or class), an optional value, an optional delta (number or preformatted string), an optional tone override (positive|negative|neutral) for the delta badge, and an optional dashed flag (line marker only). |
| orientation | string | horizontal | Lays rows out as a wrapping row or a vertical stack. |
| marker | string | circle | Swatch shape: circle, square, or line (a short dash, for line charts). A series can request a dashed stroke on the line marker with its own dashed flag. |
| label | string | Legend | Accessible name for the legend region. |
| size | default | lg | default | default renders the compact swatch/label/value row. lg renders the panel-header headline: a marker + label row, then the value in a large tabular-number style with its delta badge beside it. |
Slots
default— Unused; rows render from the items prop only.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- Renders as a description list, so each swatch/label pairs with its value the way a <dt>/<dd> pair does.
- A row with no explicit color cycles through the shared chart palette by its index.
- A row with a delta renders a small soft badge (composing badge's tone read-model) with an up/down glyph: the direction always reflects the delta's numeric sign, and the badge's tone defaults from that sign (positive up, negative down) unless the row's own tone overrides it — so a series such as an expense can mark a fall as good news without flipping the arrow.
- size="lg" restructures each row into a headline block: the marker/label pair on top, the value in a larger tabular-number style with its delta badge beside it underneath.
- Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.
Guidance
Reveal a visual trend, distribution, flow, or relationship.
Use when
- Use when trend, distribution, flow, intensity, or relationship questions are easier to answer visually than in raw numbers.
- Labelling series colours for an SVG chart, or for an ApexCharts chart rendered with its built-in legend turned off.
- Needing the legend values to read as label/value pairs, such as beside a pie or donut chart.
Avoid when
- Do not use a chart when users mainly need exact lookup or when a single metric would be clearer as text or a stat.
- The chart already renders ApexCharts' own legend; do not add a second one.
- Only a single tooltip is needed at the pointer, not a static key; use chart-tooltip instead.
Use instead
- Table for exact lookup
- Stat for a single value
Anti-patterns
- Decorative charts without a meaningful question
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- No component-owned keyboard interaction; native element behavior applies.
- Focus
none
- The root carries aria-label from the label prop, so screen readers announce the legend as a named region.
- Each swatch is aria-hidden, including the dashed line marker; the series name is still readable as plain text beside it.
- The delta badge's arrow glyph is aria-hidden with a translatable sr-only word (up/down/flat) read before the value, e.g. "up 12%".
- 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 file ui:add writes
into your app. Previews render this same code; there are no preview-only components.
@props([
// Items: each ['label' => string, 'color' => 'chart-1'|token class, 'value' => string|null,
// 'delta' => number|string|null, 'tone' => 'positive'|'negative'|'neutral'|null, 'dashed' => bool].
'items' => [],
// horizontal (a wrapping row) | vertical (a stack).
'orientation' => 'horizontal',
// circle | square | line (a short dash, for line charts).
'marker' => 'circle',
'label' => 'Legend',
// default | lg (variant). lg is the headline layout: a marker + label row,
// then the value in a large tabular-number style with its delta badge
// beside it — the panel-header key above a chart (see stat, chart-card).
'size' => 'default',
])
@php
// Shared with chart-tooltip and partition-bar.
$styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
$swatches = $styles['chart']['swatches'];
$strokes = $styles['chart']['strokes'];
$chartPalette = $styles['chart']['palette'];
$orientation = $orientation === 'vertical' ? 'vertical' : 'horizontal';
$size = $size === 'lg' ? 'lg' : 'default';
$rootLayout = match (true) {
$size === 'lg' && $orientation === 'vertical' => 'flex flex-col gap-6',
$size === 'lg' => 'flex flex-wrap items-start gap-x-8 gap-y-4',
$orientation === 'vertical' => 'flex flex-col gap-1',
default => 'flex flex-wrap items-center gap-x-4 gap-y-1',
};
@endphp
{{-- A static legend for any chart — the SVG charts, or an ApexCharts chart with
`legend.show` off. Rows are a description list so the values read as pairs.
`size="lg"` turns each row into the panel-header headline: marker + label
on top, a large value with its delta badge underneath. --}}
<dl
data-slot="chart-legend"
data-orientation="{{ $orientation }}"
data-size="{{ $size }}"
aria-label="{{ $label }}"
{{ $attributes->merge(['class' => 'text-xs '.$rootLayout]) }}
>
@foreach ($items as $i => $item)
@php
$color = $item['color'] ?? $chartPalette[$i % count($chartPalette)];
// `dashed` only reads on the line marker — a dashed stroke reads
// as a marker shape change on circle/square, not a dash pattern.
$dashed = $marker === 'line' && (bool) ($item['dashed'] ?? false);
$markerClass = match ($marker) {
'square' => 'size-3 rounded-sm',
'line' => $dashed ? 'h-0 w-4 border-t-2 border-dashed' : 'h-0.5 w-3 rounded-full',
default => 'size-2 rounded-full',
};
$markerColorClass = $dashed
? ($strokes[$color] ?? 'border-current')
: ($swatches[$color] ?? $item['color'] ?? '');
// `delta`: a number (its sign picks the direction) or a
// preformatted string (a leading -/− reads the same way, then is
// dropped — the badge's arrow already carries the direction).
// `tone` lets a series override the sign-derived colour, so an
// expense series can mark a fall as good news without flipping
// the arrow that shows what actually happened.
$delta = $item['delta'] ?? null;
$deltaText = null;
$deltaTone = null;
$deltaDirection = null;
if ($delta !== null && $delta !== '') {
$isNumeric = is_numeric($delta);
$deltaString = $isNumeric ? '' : trim((string) $delta);
$isNegative = $isNumeric
? ((float) $delta) < 0
: (str_starts_with($deltaString, '-') || str_starts_with($deltaString, '−'));
$isZero = $isNumeric && (float) $delta === 0.0;
$deltaDirection = $isZero ? 'flat' : ($isNegative ? 'down' : 'up');
$deltaTone = in_array($item['tone'] ?? null, ['positive', 'negative', 'neutral'], true)
? $item['tone']
: match ($deltaDirection) {
'up' => 'positive',
'down' => 'negative',
default => 'neutral',
};
$deltaText = $isNumeric
? rtrim(rtrim(number_format(abs((float) $delta), 1), '0'), '.').'%'
: ltrim($deltaString, '+-−');
// Fixed, hand-authored glyphs (never user data) — safe to
// print unescaped so the badge stays a single reusable line
// in both the default and lg layouts below.
$deltaGlyph = match ($deltaDirection) {
'up' => '<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>',
'down' => '<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>',
default => '<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>',
};
$deltaWord = match ($deltaDirection) {
'up' => __('up'),
'down' => __('down'),
default => __('flat'),
};
}
@endphp
@if ($size === 'lg')
<div data-slot="chart-legend-item" class="flex flex-col gap-1">
<dt class="flex items-center gap-2">
<span aria-hidden="true" class="shrink-0 {{ $markerClass }} {{ $markerColorClass }}"></span>
<span class="text-muted-foreground">{{ $item['label'] ?? '' }}</span>
</dt>
@if (isset($item['value']))
<dd class="flex items-center gap-2">
<span class="text-2xl font-semibold tabular-nums text-foreground">{{ $item['value'] }}</span>
@if ($deltaText !== null)
<x-ui.badge size="sm" :tone="$deltaTone" class="gap-1">{!! $deltaGlyph !!}<span class="sr-only">{{ $deltaWord }}</span>{{ $deltaText }}</x-ui.badge>
@endif
</dd>
@endif
</div>
@else
<div data-slot="chart-legend-item" class="flex items-center gap-2">
<dt class="flex items-center gap-2">
<span aria-hidden="true" class="shrink-0 {{ $markerClass }} {{ $markerColorClass }}"></span>
<span class="text-muted-foreground">{{ $item['label'] ?? '' }}</span>
</dt>
@if (isset($item['value']))
<dd class="font-medium tabular-nums text-foreground">{{ $item['value'] }}</dd>
@endif
@if ($deltaText !== null)
<x-ui.badge size="sm" :tone="$deltaTone" class="gap-1">{!! $deltaGlyph !!}<span class="sr-only">{{ $deltaWord }}</span>{{ $deltaText }}</x-ui.badge>
@endif
</div>
@endif
@endforeach
</dl>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.1.1 - 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