Bar chart
Purpose
The bar chart is the default Force UI component for categorical comparison. Use it when the reader is asking how a numeric value compares across a discrete, named set of categories — pipeline stages, team names, regions, build outcomes, statuses. Do NOT use a bar chart when the x axis is continuous time; use a line chart or area chart instead. Do NOT use a bar chart when only one category exists; fall back to a KPI tile. Do NOT use a bar chart when the reader's primary question is each category's share of a meaningful total; use a donut chart instead. Do NOT render a bar chart below 240px wide; use the mini variant instead.
Anatomy
Names defined here are the canonical terms reused in props, tokens, and implementation code.
Structural parts:
- Title (required at md and lg density, optional at sm): The headline label above the chart. Wraps to two lines maximum, then truncates with ellipsis.
- Subtitle (optional): A single context line below the title. One line, no wrapping.
- Headline metric (optional): A KPI tile paired with the chart when a single summary number is the primary takeaway. Contains a value, a label, and an optional delta. See the Headline metric variant.
- Plot area: The region inside the axes where bar marks are drawn.
- Y axis: Carries the numeric value scale. MUST always begin at zero. Tick labels are abbreviated at md density and below (e.g., 1.2k instead of 1,200).
- X axis: Carries the discrete category labels. Labels rotate 45 degrees when their combined width would exceed the plot area. At sm density, labels abbreviate first, then drop to tooltip-only mode.
- Data marks (bars): One rectangle per category in single; one per category per series in grouped; one stacked segment per series within a category in stacked. All four corners are rounded using
--force-radius-sm. - Gridlines: Horizontal lines at regular Y-axis tick intervals crossing the full width of the plot area. Typically four to five lines including the zero baseline.
Supporting parts:
- Legend: Required at md and lg whenever more than one series is shown. Always positioned below the chart. Omit for single-series charts. Each item uses a 14×14px filled square swatch in the series color followed by the series name.
- Tooltip: On hover or keyboard focus, appears near the cursor position with a ~12px offset, tracking the pointer as it moves across the chart. Repositions to stay within the chart container when near a boundary. Contains the category label, then one row per series showing a color swatch, the series name, and the formatted value. In single-series charts the series name row is omitted. Disappears when the pointer or focus leaves the chart container.
- Bar value label: The numeric value for a bar. Surfaced inside the tooltip on hover or focus. It is NOT a persistent always-on label — values are never rendered floating above bars in a resting state.
- Reference line (optional): A dashed horizontal threshold or average line overlaid on the plot area. Label is right-aligned at the line's Y position, outside the plot area boundary.
- Action menu: A three-dot overflow button in the top-right corner of the chart header, exposing per-widget actions (export, refresh, filter). Visible only when
exportableis true or filter actions are present. - Empty / loading / error overlays: Replace the plot area when data is unavailable. The title, subtitle, headline metric, and action menu remain visible in every overlay state.
- No-permission state: Replaces the entire chart including axes and plot area when the viewer cannot access the underlying data.
Data requirements
| Requirement | Constraint | Behavior at the limit |
|---|---|---|
| Required fields | category (string), value (numeric). Optional series field for grouped or stacked. |
If category or value is missing, render the Empty state with an explanation. |
| Accepted types | Strings or string-coercible categories. Numeric values. ISO datetime strings accepted when categories are time buckets. | Numbers passed as categories are coerced to strings. Mixed types in the same field are rejected and render the Error state. |
| Minimum data points | 2 categories. | Below 2, fall back to a KPI tile. Do NOT render a lone bar. |
| Maximum data points | 20 categories at md density; 50 categories in the horizontal variant with internal scroll at lg density. | Above the limit, group the tail into "Other" or paginate, with a banner explaining the truncation. |
| Series count limit | 4 series for grouped; 5 series for stacked. | Above the limit, redirect to small multiples or aggregate the long tail into "Other." |
| Null / missing values | Treated as zero. The category retains a 1px baseline mark to preserve alignment. | Never silently drop a category — a gap distorts the comparison. |
| Zero / negative values | Allowed in single and grouped variants. Rejected in stacked. | A stacked payload containing negatives renders the Error state with a message redirecting to a line chart. |
| Sort order | Preserved as provided unless sortBy is passed. The chart never re-sorts silently. |
sortBy accepts "value-desc", "value-asc", "category-asc", or "as-provided". |
| Aggregation | None on the client. The caller provides values at the desired bucket granularity. | Duplicate categories in the input trigger the Error state. |
Configuration
| Prop | Type | Default | Purpose |
|---|---|---|---|
title |
string | required | Headline label. Wraps to two lines then truncates. |
subtitle |
string? | — | Optional one-line context below the title. |
headlineMetric |
{ value, label, delta }? |
— | Optional KPI rendered alongside the chart. |
data |
array | required | Data payload. Each item must contain category and value. Add series for grouped or stacked variants. |
groupBy |
string? | — | Field name used to split data into series. |
referenceLines |
{ value, label, style }[]? |
[] |
Threshold or average lines overlaid on the chart. |
variant |
'vertical-single' | 'vertical-grouped' | 'vertical-stacked' | 'horizontal-single' | 'horizontal-stacked' | 'mini' |
'vertical-single' |
The chart variant. See Variants. |
colorMapping |
object? | system | Overrides the system categorical color assignment for this chart instance. |
density |
'sm' | 'md' | 'lg' |
'md' |
Affects font sizes, padding, bar thickness, and label rotation thresholds. |
theme |
'light' | 'dark' | 'auto' |
'auto' |
Overrides the dashboard theme for this chart instance. |
sortBy |
'value-desc' | 'value-asc' | 'category-asc' | 'as-provided' |
'as-provided' |
Controls category render order. |
legend |
'bottom' | 'none' | 'auto' |
'auto' |
Auto resolves to none for single-series, bottom for all multi-series charts. |
normalized |
boolean | false |
Horizontal-stacked only. When true, all bars span the full plot width and values are displayed as percentage shares. Ignored for all other variants. |
tooltip |
boolean | true |
Whether hover or focus reveals the tooltip. |
onMarkClick |
function? | — | Drill-down callback fired with the full data row. When set, bars become focusable interactive elements with role="button". |
exportable |
boolean | true |
Whether the action menu exposes an Export option. |
Variants
Vertical single (default)
One bar per category. The most common configuration. Use when comparing a single numeric value across discrete categories. Bars grow vertically from the zero baseline upward.
Vertical grouped
Side-by-side bars for each series within a category. Use when comparing two to four series across the same set of categories. The gap between bars within a group is narrower than the gap between groups so grouping is visually clear. Do NOT use grouped with five or more series — aggregate the excess into "Other" or switch to small multiples.
Vertical stacked
Bars subdivided by series segments, stacked from the baseline upward. Use when the composition of each category total is the primary takeaway — how much each series contributes to the whole. Maximum 5 series. Legend is always required and may not be set to none. Do NOT use stacked when the data contains negative values; render the Error state and redirect to a line chart instead.
Horizontal single
A bar chart rotated 90 degrees: categories run along the Y axis, values along the X axis, and bars extend left to right. Use when category labels are long strings (product names, region identifiers, service names) that would require rotation or truncation in a vertical layout. Use when the category count exceeds 20, up to a maximum of 50 with internal scroll.
Axes: The Y axis carries the discrete category labels, left-aligned in a fixed-width column. The X axis carries the numeric value scale and MUST always begin at zero. Gridlines are vertical lines crossing the plot area at regular X-axis tick intervals.
Category label column: Labels truncate with ellipsis when they exceed the column's maximum width. Maximum widths by density: 100px at sm, 160px at md, 200px at lg. Full labels are always available in the tooltip regardless of truncation.
Bar height by density: The dimension of each bar perpendicular to the value axis is fixed, not proportional to available space: 20px at sm, 24px at md, 32px at lg. The gap between bars is 4px at sm, 8px at md, and 8px at lg.
Overflow: When categories exceed the visible height of the chart container, the plot area scrolls internally. The chart container height is fixed by the dashboard layout. Category labels and the X axis header remain visible outside the scroll region.
Tooltip: Follows the cursor (same behavior as all variants). The tooltip does not flip orientation in horizontal charts.
Legend: Always positioned below the chart. Omit for single-series charts.
Reference lines: Rendered as vertical dashed lines crossing the full height of the plot area at the specified X-axis value. Label sits top-aligned outside the plot area above the line. Do NOT use reference lines in the mini variant.
Horizontal stacked
The horizontal equivalent of vertical-stacked. Bars extend left to right; series segments subdivide each bar horizontally from the baseline. Use when long category labels and composition comparison are needed simultaneously — the reader wants to see both how categories rank and how each category's total breaks down by series.
All rules from Horizontal single apply (label truncation, bar height by density, internal scroll, cursor-following tooltip, legend always below) plus the following:
Segment order: Series 1 begins at the left baseline; subsequent series stack to the right in the same order as vertical-stacked stacks bottom-to-top. The leftmost segment is always the first series.
Maximum series: 5. Above 5, aggregate the excess into "Other."
Negative values: Not allowed. A stacked payload containing negatives renders the Error state with a message redirecting to a line chart.
Legend: Always required. May not be set to none.
Normalized mode (normalized: true): When enabled, all bars span the full width of the plot area regardless of their absolute total. Segment widths represent each series' percentage share of the category total. The X axis label format changes to "0% — 100%". The tooltip shows percentage values instead of absolute values. sortBy is ignored in normalized mode — category order is determined solely by the data or as-provided. Do NOT use normalized with any variant other than horizontal-stacked; the prop is silently ignored elsewhere.
Mini bar (inline)
A compact bar chart with no axes, no legend, no title, and no padding. Used inside table cells or narrow dashboard tiles to show distribution at a glance. Bars are always horizontal in this variant. Maximum row height: 32px. The bar group fills the available cell width proportionally.
Do NOT use the mini variant as a standalone chart — it has no accessible context of its own. The surrounding table column header or tile label must provide the descriptive context.
With headline metric
Any vertical variant may be paired with a single prominent KPI number by passing headlineMetric. The number occupies a fixed-width column to the left of the chart; a vertical divider separates it from the plot area. The number is the takeaway; the bars show the breakdown. Do NOT use headline metric with the horizontal or mini variants.
With reference line
Any vertical or horizontal variant may include one or more threshold or average lines by passing referenceLines. Each line crosses the full width of the plot area at the specified value. The label sits right-aligned outside the plot area at the line's Y position. Use when every category must be compared against a fixed target. Do NOT use reference lines in the mini variant.
States
Every state except No-permission is a substitute for the plot area only. The title, subtitle, headline metric, and action menu always remain visible.
Empty: The data fetch succeeded but returned zero rows. Render the zero baseline and empty gridlines so the chart skeleton remains visible. Show a centered message explaining why data is absent (e.g., "No data for the selected period") and, where relevant, a call-to-action to broaden the filter or connect a data source. Do NOT show a blank white rectangle.
Loading: A data fetch is in progress. Render skeleton bars at varying heights that imply a bar chart shape. Do NOT use a Spinner — the shaped skeleton preserves spatial context. Use the Skeleton component styled to --force-color-bg-muted.
Error: The data fetch failed or the payload was structurally invalid (e.g., negative values in a stacked variant, duplicate categories). Show a centered error icon, a brief explanation, and a "Retry" secondary button. Do NOT silently fall back to the Empty state — the distinction between "no data" and "failed fetch" is meaningful.
No permission: The viewer lacks access to the underlying data. Replace the entire chart including plot area, axes, and title with a lock icon, the message "You do not have access to this data," and a prompt to contact an administrator. Do NOT reveal any data values, category labels, or axis scale.
Partial data: Some categories or series are present but others are missing. Render the available bars. Show a muted inline banner directly below the title explaining what is absent (e.g., "Cache and Network stages missing for this period"). Do NOT fill the missing positions with empty bars.
Single data point: The payload contains exactly one category. Fall back to the KPI tile component, passing the value and category label through. Do NOT render a lone bar — there is nothing to compare against.
Behavior
Hover / focus: When a pointer enters a bar or keyboard focus lands on a bar, that bar renders at full opacity and all sibling bars dim to --force-opacity-subtle (0.7). The tooltip appears. On touch, a tap opens the tooltip; a second tap closes it.
Tooltip: Follows the cursor position across the chart area, appearing ~12px from the pointer. Repositions to stay within the chart container when near a boundary — flipping from right to left of the cursor, or from below to above, as needed. On keyboard focus, positions relative to the focused bar. Disappears when focus or pointer leaves the chart container.
Bar click: No-op by default. When onMarkClick is provided, clicking or pressing Enter on a focused bar fires the callback with the full data row. Bars in this mode render with a pointer cursor.
Legend toggle: Clicking a legend item toggles that series' visibility. The toggled-off item renders at --force-opacity-subtle. In stacked, the remaining segments reflow to fill the freed height. In grouped, the freed column space collapses.
Zoom / pan: Not supported. Bar charts are not zoomable. When data exceeds the visible category count, switch to the horizontal variant with internal scroll.
Cross-filter: Off by default. When enabled at the dashboard level, selecting a bar updates all co-bound widgets.
Keyboard navigation: Tab moves focus to the chart container. In vertical variants, ArrowLeft / ArrowRight step through bars by category; ArrowUp / ArrowDown step between series in grouped or stacked variants. In horizontal variants, ArrowUp / ArrowDown step through bars by category; ArrowLeft / ArrowRight step between series in stacked. The tooltip opens at each stop. Enter fires onMarkClick if set. Escape returns focus to the chart container.
Animation: On initial render and data updates, bars grow from the baseline to their target height over --force-duration-normal using --force-easing-standard. When prefers-reduced-motion: reduce is active, bars render immediately at their final height with no animation.
Accessibility
Color contrast: All token pairings in this spec resolve to WCAG 2.2 AA compliant values by default. Custom colorMapping overrides are invalid if they break contrast between the bar fill and chart background, or between any text and its background.
Color-blind safety: The six chart colors (--force-color-chart-1 through --force-color-chart-6) pass Deuteranopia, Protanopia, Tritanopia, and Achromatopsia simulations.
Open: fill patterns for 3+ series. When a grouped or stacked chart renders three or more series, color alone is insufficient for color-blind users at narrow bar widths. The spec requires that bars also use distinct fill patterns (hatching, diagonal lines, dots, or similar). The specific patterns have not yet been designed. Grouped and stacked variants with 3+ series MUST NOT ship as WCAG-compliant until this is resolved.
Screen reader pattern: The chart element carries an accessible name derived from title (e.g., aria-label="Bar chart: Failed builds by stage, last 30 days"). A summary sentence is exposed via aria-description (e.g., "5 categories. Highest: Backend at 45."). A "View as table" affordance is reachable from the chart container's focus ring; activating it replaces the SVG with a <table> of the underlying data.
Keyboard reachability: Every pointer interaction must also be achievable by keyboard. Tab order: chart container → bars left to right → legend items. Focus must not become trapped inside the chart.
Focus indication: A visible focus ring (--force-shadow-focus-ring + --force-color-border-focus) appears on the chart container, on each legend item, and on each individual bar mark. The focused bar's ring is visually distinct from its hover highlight.
Reduced motion: When prefers-reduced-motion: reduce is set, all bar entry and update animations are removed. Tooltips and focus states appear immediately.
Text scaling: The chart reflows without clipping up to 200% browser text size. Category label degradation order: render at default size → rotate 45° → abbreviate → drop to tooltip-only mode.
Sizing and responsive behavior
| Density | Container width | Behavior |
|---|---|---|
| sm | < 320px | Drop axis titles. Abbreviate category labels. Narrow bar thickness. No visible legend — series names accessible via tooltip only. |
| md | 320–599px | Compact axis labels. Numbers abbreviated (k, M). Legend wraps below chart. Tooltip shows one row per series. |
| lg | ≥ 600px | Full-length axis labels. Multiple gridlines. Legend below the chart. Tooltip shows extended metadata. |
The minimum supported width for any non-mini bar chart is 240px. Below that, the chart MUST render the Empty state with the message "Widget too narrow to display" rather than attempting to render bars.
Guidance
Always start the Y axis at zero. A bar's height encodes its value — truncating the axis exaggerates differences and misleads the reader. Do NOT set a non-zero Y minimum. If tight data ranges matter, add a reference line at the meaningful threshold and keep the baseline at zero.
Sort to match the reader's question. If the question is "which category is largest?", default to sortBy: "value-desc". If the question is "how does each named pipeline stage perform?", preserve the natural order. Do NOT default to alphabetical order when value ranking is more informative.
Cap the category count before bars crowd the chart. At md density, 20 categories is the maximum for vertical variants. Above that, group the tail into "Other" or switch to the horizontal variant with internal scroll. Do NOT squeeze more than 20 bars into a fixed-width vertical widget — individual bars become unreadable and the comparison is lost.
Use one chart per unit. All bar values must share the same unit. Mixed units (dollars alongside counts) belong in separate charts. Do NOT add a secondary Y axis; use a combo chart pattern when a second scale is genuinely required.
One primary action per chart context. Do NOT place two primary buttons (e.g., Export and Filter) as equal-weight calls-to-action in the action menu. Destructive actions in the menu (e.g., Delete widget) must be separated by a divider and placed last.
Token usage
Bar marks
- Fill, series 1–6:
--force-color-chart-1through--force-color-chart-6, assigned in sequence - Border radius (all four corners):
--force-radius-sm - Focused bar: full opacity (no change)
- Unfocused sibling bars while a bar is hovered or focused:
--force-opacity-subtle(0.7) on the bar fill
Plot area and axes
- Chart surface background:
--force-color-bg-surface - Gridlines:
--force-color-border-muted, 1px solid - Zero baseline:
--force-color-border-default, 1px solid (slightly stronger than gridlines to anchor the scale) - Axis tick labels:
--force-color-text-tertiary,--force-font-size-xs,--force-font-weight-regular - Axis title (if present):
--force-color-text-secondary,--force-font-size-sm,--force-font-weight-medium
Chart header
- Title:
--force-color-text-primary,--force-font-size-xl,--force-font-weight-semibold - Subtitle:
--force-color-text-tertiary,--force-font-size-sm,--force-font-weight-regular
Tooltip
- Background:
--force-color-bg-inverse - Text:
--force-color-text-on-inverse - Elevation:
--force-shadow-sm - Border radius:
--force-radius-md - Category label:
--force-font-size-sm,--force-font-weight-medium - Value rows:
--force-font-size-sm,--force-font-weight-regular
Legend
- Swatch dimensions: 14×14px filled square
- Swatch fill: matches the series
--force-color-chart-N - Label text:
--force-color-text-secondary,--force-font-size-sm,--force-font-weight-regular - Toggled-off item:
--force-opacity-subtle(0.7) on the entire legend item
Reference line
- Stroke color:
--force-color-border-muted - Stroke width: 1px
- Dash pattern:
2px 2px - Label:
--force-color-text-tertiary,--force-font-size-xs,--force-font-weight-regular
Focus ring
- Chart container, bar marks, legend items:
--force-shadow-focus-ring+--force-color-border-focus
Motion
- Bar entry and data update:
--force-duration-normal(250ms),--force-easing-standard - Tooltip appear:
--force-duration-fast(150ms),--force-easing-standard
Related components
| Component | Relationship | When to redirect |
|---|---|---|
| Line chart | Continuous-time alternative | When the x axis is continuous time rather than discrete named categories. |
| Donut chart | Part-to-whole alternative | When the reader's primary question is each category's percentage share of a total. |
| KPI tile | Single-value fallback | When only one category exists or a single summary number is the entire takeaway. |