Skip to content

Bullet Chart

ChartsSince 0.4.0
import { BulletChart } from '@absoluteui/components/bullet-chart';
import '@absoluteui/components/bullet-chart/style.css';

Using Angular? See Bullet Chart for Angular.

An accessible SVG bullet chart: compact horizontal measures against a target and qualitative ranges (for example poor / satisfactory / good). It provides:

  • one row per measure on a shared value axis: up to three shaded range bands, a measure bar growing from zero, and a target tick;
  • ranges in three neutral shades (not palette colors), named in each row’s accessible label, tooltip, and table (rangeLabels), so meaning never depends on shading;
  • keyboard-reachable measure bars (one tab stop, Up/Down between rows) whose names read value, target, and range, with a tooltip on focus and hover;
  • a generated summary (every measure, and how many are at or above target) and a data table fallback (Measure, Value, Target, Range).

No animation.

  • Key performance indicators against targets in a dashboard, with a qualitative scale.
  • Replacing gauges and meters with a compact, comparable row layout.
  • Measures on very different scales (all rows share one value axis): use one chart per scale or Stat Card.
  • Trends over time: use Line Chart or Sparkline.
  • A single value against a maximum without target or ranges: use Progress Bar or Gauge.

Measures against targets with named ranges

Section titled “Measures against targets with named ranges”
import { BulletChart } from '@absoluteui/components/bullet-chart';
const host = document.createElement('div');
document.body.append(host);
BulletChart.create(host, {
label: 'Quarterly goals',
measures: [
{ id: 'revenue', label: 'Revenue', value: 270, target: 250, ranges: [150, 225, 300] },
{ id: 'profit', label: 'Profit', value: 22, target: 26, ranges: [20, 25, 30] },
{ id: 'orders', label: 'New orders', value: null, target: 180, ranges: [100, 150, 200] },
],
rangeLabels: ['Poor', 'Satisfactory', 'Good'],
height: 180,
});

Percent values on a fixed domain, live update

Section titled “Percent values on a fixed domain, live update”
import { BulletChart, type BulletMeasure } from '@absoluteui/components/bullet-chart';
const host = document.querySelector<HTMLElement>('#sla');
if (host) {
const measures: BulletMeasure[] = [
{ id: 'uptime', label: 'Uptime', value: 0.994, target: 0.999, ranges: [0.99, 0.995, 1] },
{ id: 'first-response', label: 'First response in 1 h', value: 0.87, target: 0.9, ranges: [0.8, 0.9, 1] },
];
const chart = BulletChart.create(host, {
label: 'Service levels',
measures,
rangeLabels: ['Breach', 'At risk', 'Met'],
valueFormat: { style: 'percent', maximumFractionDigits: 1 },
valueAxis: { min: 0.75, max: 1 },
color: 2,
});
chart.update({ measures: measures.map((m) => (m.id === 'uptime' ? { ...m, value: 0.9991 } : m)) });
}
import { BulletChart } from '@absoluteui/components/bullet-chart';
import '@absoluteui/components/bullet-chart/style.css';
const host = document.querySelector<HTMLElement>('#kpis');
if (host) {
BulletChart.create(host, {
label: 'Customer satisfaction',
measures: [
{ id: 'csat', label: 'CSAT', value: 4.3, target: 4.5, ranges: [3, 4, 5] },
{ id: 'nps', label: 'NPS / 20', value: 2.4, target: 2.5, ranges: [1, 2, 5] },
],
rangeLabels: ['Poor', 'Fair', 'Good'],
valueAxis: { min: 0, max: 5 },
height: 160,
});
}
import { BulletChart } from '@absoluteui/components/bullet-chart';
const host = document.createElement('div');
document.body.append(host);
BulletChart.create(host, {
label: 'Quarterly goals',
measures: [
{ id: 'rev', label: 'Revenue', value: 270, target: 250, ranges: [150, 225, 300] },
{ id: 'profit', label: 'Profit', value: 22, target: 26, ranges: [20, 25, 30] },
],
rangeLabels: ['Poor', 'Satisfactory', 'Good'],
});
<div class="aui-chart aui-bullet-chart [aui-chart--patterns]">
<div class="aui-chart__header">title, description</div>
<ul class="aui-chart__legend" hidden/>
<div class="aui-chart__body">
<svg class="aui-chart__svg" role="group" aria-roledescription="bullet chart" aria-labelledby="T" aria-describedby="S">
<g class="aui-chart__grid" aria-hidden="true"/> <g class="aui-chart__axis aui-chart__axis--value" aria-hidden="true"/>
<g class="aui-bullet-chart__rows aui-chart__series--c1">
<g class="aui-bullet-chart__row">
<g class="aui-bullet-chart__ranges" aria-hidden="true">
<rect class="aui-bullet-chart__range aui-bullet-chart__range--1"/> ... --2, --3
</g>
<rect class="aui-chart__bar aui-bullet-chart__measure aui-chart__mark" role="img" aria-label="Revenue: 270, target 250, Good"/>
<line class="aui-bullet-chart__target" aria-hidden="true"/>
</g>
</g>
<g class="aui-chart__axis aui-chart__axis--category" aria-hidden="true"><text class="aui-bullet-chart__label">Revenue</text></g>
</svg>
<div class="aui-chart__tooltip" aria-hidden="true" hidden/>
</div>
<p class="aui-chart__summary" hidden/> <div class="aui-chart__data">Show data table, table</div>
</div>
BulletChart.create(host: HTMLElement, options: BulletChartOptions): BulletChart

Global configuration key: bulletChart (see Configuration).

Option Type Default Description
label string Chart title: visible text and the accessible name of the plot (aria-labelledby). Must not be blank. Required.
hideLabel boolean false Visually hide the title; it stays the accessible name.
description string Visible description below the title, also referenced by aria-describedby (text only).
summary string Replaces the generated accessible summary (measure count, each measure’s name, and how many are at or above target).
valueFormat Intl.NumberFormatOptions Number format for values and targets in names, the tooltip, the data table, the summary, and axis ticks (ticks use fixed fraction digits).
formatValue (value: number) => string Custom value text; overrides valueFormat everywhere except axis ticks.
hiddenSeries readonly string[] Shared chart option; validated but has no effect (a bullet chart has no toggleable series).
measures readonly BulletMeasure[] Rows: { id, label, value: number | null, target?: number, ranges?: number[] }. Ids are unique and non-empty; null, NaN, and infinities are a missing value; target is finite; ranges are up to three strictly ascending finite upper ends (the first range starts at the axis minimum). Required.
rangeLabels readonly string[] Names of the qualitative ranges in the same order as ranges (up to three, for example [“Poor”, “Satisfactory”, “Good”]); read in each measure’s name, tooltip, and table. Default “Range 1”, “Range 2”, “Range 3”.
color ChartColor (1-8) 1 Palette slot of the measure bars.
valueAxis BulletChartValueAxis ({ label?: string; min?: number; max?: number; tickCount?: number; includeZero?: boolean }) Fixed domain (max > min; marks are clamped to it) and approximate tick count (2-20). The automatic domain always includes zero and spans every value, target, and range end. label and includeZero are validated but not drawn or used.
legend 'auto' | 'show' | 'hide' "auto" Shared chart configuration; a bullet chart never shows a legend (ranges are named in each row’s label). Globally configurable.
dataTable 'toggle' | 'visible' | 'hidden' | 'none' "toggle" Data table fallback (Measure, Value, Target, Range): behind a Show data table button (built on first open), always visible, visually hidden (screen readers only), or none. Globally configurable.
tooltip boolean true Tooltip on pointer hover and keyboard focus (value, target, range). Globally configurable.
patterns boolean false Shared chart configuration; toggles the aui-chart–patterns host class. Bullet charts draw no textures (one measure color; ranges are neutral shades named in text). Globally configurable.
height number 300 Plot height in CSS px (integer 120-2000), divided evenly between rows; the width follows the container. Globally configurable.
locale string "en-US" Number and message locale. Globally configurable. Inherited from common configuration.
direction 'ltr' | 'rtl' "ltr" rtl mirrors the value axis, moves labels to the right, and swaps ArrowLeft/ArrowRight. Globally configurable. Inherited from common configuration.
Property Type Description
host HTMLElement The host element.
element SVGSVGElement The plot (svg role=“group”), named by the title and described by the summary.
summary string The generated (or custom) accessible summary.
hiddenSeries string[] Always empty (a bullet chart has no toggleable series).
Method Description
update(options: Partial<BulletChartOptions>): void Merge, validate, and re-render. Invalid options throw and keep the previous state. Keyboard focus stays on the same measure (by id).
setSeriesVisible(id: string, visible: boolean): void Shared chart method; re-renders but has no visible effect on a bullet chart. Emits no event.
destroy(): void Remove rendering, host classes, listeners, the resize observer, and the configuration subscription.

All rules live in the aui.components cascade layer, so application styles override them without specificity battles.

Class Purpose
aui-chart Host (shared chart chrome).
aui-bullet-chart Host modifier for this chart type.
aui-chart--patterns patterns option on (no textures are drawn by a bullet chart).
aui-chart__header / __title / __title--hidden / __description / __toolbar Title and description; the toolbar stays hidden.
aui-chart__legend Legend list; always hidden for a bullet chart.
aui-chart__body / __svg / __layer Plot container (height from –aui-chart-height), SVG, and drawing layer.
aui-chart__grid / __gridline / __zero-line / __axis / __axis--value / __axis--category / __tick-label Value axis and the measure label column (aria-hidden).
aui-chart__series--c1 ... --c8 Palette slot class on the rows group (color option); sets –aui-chart-color from color-chart-<n>.
aui-chart__mark / __mark--active / __mark--hover Focusable measure bar and its states.
aui-chart__bar Measure bar paint (series color with a surface-colored separator; text-colored outline on hover and focus).
aui-chart__tooltip / __tooltip-title / __tooltip-rows / __tooltip-row / __tooltip-label / __tooltip-value / __swatch Tooltip (aria-hidden; the focused bar carries the same text).
aui-chart__empty Empty state text (No data).
aui-chart__summary Hidden summary referenced by aria-describedby.
aui-chart__data / __data--hidden / __table-toggle / __table-scroll / __table / __cell--numeric Data table fallback.
aui-bullet-chart__rows / __row All rows, and one group per measure.
aui-bullet-chart__ranges / __range / __range--1 / __range--2 / __range--3 Decorative (aria-hidden) qualitative range bands, darkest first.
aui-bullet-chart__measure The focusable measure bar.
aui-bullet-chart__target Target tick (aria-hidden).
aui-bullet-chart__label Measure label beside the row (aria-hidden).

Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.

--aui-border-width | --aui-color-border | --aui-color-border-strong | --aui-color-chart-1 | --aui-color-chart-2 | --aui-color-chart-3 | --aui-color-chart-4 | --aui-color-chart-5 | --aui-color-chart-6 | --aui-color-chart-7 | --aui-color-chart-8 | --aui-color-chart-axis | --aui-color-chart-grid | --aui-color-focus-ring | --aui-color-hover | --aui-color-link | --aui-color-surface | --aui-color-surface-sunken | --aui-color-text | --aui-color-text-muted | --aui-duration-fast | --aui-easing-standard | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-sans | --aui-font-size-md | --aui-font-size-sm | --aui-font-size-xs | --aui-font-weight-medium | --aui-font-weight-regular | --aui-font-weight-semibold | --aui-line-height-normal | --aui-line-height-tight | --aui-radius-md | --aui-radius-sm | --aui-shadow-md | --aui-space-1 | --aui-space-2 | --aui-space-3 | --aui-tooltip-max-width | --aui-touch-target-min

type BulletChartConfig = ChartBaseConfig;
interface BulletMeasure { id: string; label: string; value: number | null; target?: number; ranges?: readonly number[] }
interface BulletChartOptions extends ChartCommonOptions, Partial<BulletChartConfig> {
measures: readonly BulletMeasure[];
rangeLabels?: readonly string[];
color?: ChartColor;
valueAxis?: BulletChartValueAxis; // { label?; min?; max?; tickCount?; includeZero? }
}

Pattern: SVG bullet chart as a named, described group (role=“group” with aria-roledescription “bullet chart”) of role=“img” measure bars with roving focus (Up/Down between rows); data table fallback (native table: Measure, Value, Target, Range).

Key Context Action
ArrowDown / ArrowUp Measure focused Next / previous row.
ArrowRight / ArrowLeft Measure focused Also next / previous row (swapped in RTL).
Home / End Measure focused First / last row.
PageUp / PageDown Measure focused Ten rows back / forward (clamped to the first / last).
Escape Tooltip visible Hides the tooltip without moving focus (WCAG 1.4.13); it returns on the next move.
Tab / Shift+Tab Any One tab stop for the plot (the active measure), then the Show data table button and table region.
Element Role
svg.aui-chart__svg group with aria-roledescription (localized “bullet chart”), named by the title
measure bars img with an aria-label: measure name, formatted value, target, and the named qualitative range
data table table (native) with caption, column headers, and row headers; wide tables scroll in a focusable region
range bands, target ticks, measure labels, axis, tooltip aria-hidden (decorative duplicates of the accessible data)
Attribute Element Value
aria-labelledby svg id of the visible (or visually hidden) title
aria-describedby svg description id (when provided) and the generated summary id
aria-roledescription svg Localized chart type, “bullet chart”
aria-label each measure bar Localized “{label}: {value}”, then “, target {target}” when a target is set, then “, {range}” when ranges are set (for example “Revenue: 270, target 250, Good”); missing values read “No value”
tabindex active measure bar 0 on exactly one bar (roving); other bars have none
aria-expanded / aria-controls Show data table button Table open state and the table region id

Roving focus: one measure bar is in the tab sequence and remembers the last visited measure (by id), also across re-renders (resize, update). Toggling the table keeps focus on the table button.

Criterion Level Note
1.1.1 Non-text Content A
1.3.1 Info and Relationships A
1.4.1 Use of Color A
1.4.3 Contrast (Minimum) AA
1.4.10 Reflow AA
1.4.11 Non-text Contrast AA
1.4.13 Content on Hover or Focus AA
2.1.1 Keyboard A
2.4.3 Focus Order A
2.4.7 Focus Visible AA
4.1.2 Name, Role, Value A

Automated test results: Accessibility and compliance.

  • Qualitative ranges use three neutral shades (not palette colors) and are named in each row’s accessible label, tooltip, and table (rangeLabels, default “Range 1”…“Range 3”), so meaning never depends on shading.
  • The measure falls in the first range whose upper end is at or above the value; values above the last range end read as the last range.
  • The generated summary lists the measure count, every measure’s label text, and how many measures with a target are at or above it (for example “1 of 2 at or above target.”).
  • The target tick is a 3 px text-colored line across the band; the measure bar is a third of the band height.
  • Forced colors: ranges become Canvas with GrayText outlines, targets and labels CanvasText; focus and hover use Highlight.
  • No animation: nothing moves on render; hover and focus emphasis transitions are removed under prefers-reduced-motion and animation: none.
  • Manual screen-reader verification (NVDA, JAWS, VoiceOver, TalkBack) is pending.
  • aria-roledescription support varies; screen readers without it announce “group”.
  • Range band boundaries themselves are not exposed; only the range the value falls in is named.

default, empty (“No data”: no measures), measure missing, measure focused (tooltip), row hovered, table open.

From Trigger To
plot not focused Tab active measure focused, tooltip shown
measure focused arrow keys, Home, End, PageUp, PageDown another measure focused
tooltip visible Escape tooltip hidden
table closed Show data table table shown
any update({ measures }) re-rendered; focus stays on the same measure id
  • Ranges are neutral shades (22%, 13%, 6% of color-text over color-surface, darkest first) and are always named in text; pass rangeLabels so rows read “Good” rather than “Range 3”.
  • The value falls in the first range whose upper end is at or above it; values above the last range read as the last range.
  • Summary example ends with “1 of 2 at or above target.” (measures with a target and a value).
  • Forced colors: ranges Canvas with GrayText outlines; targets and labels CanvasText; focus and hover Highlight.
  • No animation. Manual screen-reader verification is pending.

Width follows the container; labels take up to 35% of the width and are truncated with an ellipsis. Bands are at most 40 px tall; the measure bar is a third of the band. Reflows at 320 CSS px.

The value axis is mirrored (bars grow leftward from zero), labels move to the right, ArrowLeft/ArrowRight swap.

Range shades derive from color-text and color-surface, so they follow every theme. The measure bar uses one palette slot (color, default color-chart-1).

configureAbsoluteUI({ components: { bulletChart: { height: 200, dataTable: 'visible' } } });

Instance options override global and theme defaults. All options are dynamic.

All text uses textContent; SVG uses createElementNS; colors are classes; the height and tooltip position use the CSSOM. No innerHTML or style attributes.

No persistence, network access, or telemetry. Values are not logged; validation errors name the option, never its value.

Budget: create with 30 measures < 30 ms in headless Chromium (e2e performance test), including the first render.

Up to about 30 measures per chart; raise height with the row count (bands shrink with the row height).

  • No measures: “No data”.
  • Missing value (null, NaN, infinity): no bar (a zero-width focusable mark), “No value” in its name and table; excluded from the at-or-above-target count.
  • No target: no tick, “-” in the table. No ranges: no bands, “-” in the Range column.
  • Negative values: bars grow from zero toward the negative side; the domain includes zero.
  • A fixed valueAxis domain clamps bars, targets, and bands to its edges.
  • Fewer rangeLabels than ranges: the rest read “Range {n}”.
Code Category Cause
BULLET_CHART_OPTIONS configuration Blank label; measures not an array; measure not an object; empty or duplicate id; label not a string; value not a number or null; target not finite; more than 3 ranges or not strictly ascending finite numbers; more than 3 rangeLabels or not strings; color not 1-8; invalid valueAxis (max <= min, tickCount outside 2-20).
CFG_INVALID_VALUE / CFG_UNKNOWN_OPTION configuration Invalid or unknown configuration.

A failed construction leaves the host untouched.

  • Horizontal rows on one shared axis; one measure color; at most three ranges.
  • legend, patterns, hiddenSeries, valueAxis.label, and valueAxis.includeZero have no effect.
  • Manual screen-reader verification is pending.
  • Vertical bullets, per-row axes, comparative (second) measures.
  • More than three ranges.
  • HTML in labels.