The chart is a standalone component. Import the styles once for the whole family.

bar-chart.ts
import { KuiBarChart } from '@kikita-labs/ui';

Pass series aligned with categories. The bars share the series, categories, axes, formatting and keyboard mechanics of the line chart.

<div class="chart-example">
  <kui-bar-chart ariaLabel="MRR by plan" [series]="series" [categories]="categories" />
</div>

stacked stacks the series within each category instead of placing them side by side. Positive and negative values stack separately, so a category with gains and losses shows two stacks growing in opposite directions.

<div class="chart-example chart-example--stack">
  <kui-bar-chart ariaLabel="Grouped by day" [series]="series" [categories]="categories" size="sm" />
  <kui-bar-chart
    ariaLabel="Stacked by day"
    [series]="series"
    [categories]="categories"
    [stacked]="true"
    size="sm"
  />
</div>

orientation horizontal flips the on-screen bars only. axes.x and axes.y stay semantic (x is the categories, y the values), whichever edge shows them.

<div class="chart-example">
  <kui-bar-chart
    ariaLabel="MRR by plan"
    orientation="horizontal"
    [series]="series"
    [categories]="categories"
    [axes]="axes"
  />
</div>

patterns fills each series with one of eight hatch patterns instead of plain colour, so series differ without relying on colour.

<div class="chart-example">
  <kui-bar-chart
    ariaLabel="Sessions and sign-ups"
    [series]="series"
    [categories]="categories"
    [patterns]="true"
  />
</div>

loading shows a silhouette of bars with varying heights that shrinks with its container; an empty series shows the empty composition.

No data
<div class="chart-example chart-example--stack">
  <kui-bar-chart
    ariaLabel="Loading MRR"
    [series]="series"
    [categories]="categories"
    [loading]="true"
    size="sm"
  />
  <kui-bar-chart ariaLabel="Empty MRR" [series]="[]" size="sm" />
</div>

Charts normalize their input before drawing, so gaps and invalid values never become zeros.

  • series[].data is aligned index for index with categories; when the lengths differ the shorter side wins.
  • null, NaN and Infinity are gaps: the mark is skipped and a line breaks there instead of connecting across. A gap is never drawn as 0.
  • The value domain is [min(0, min), max(0, max)] over all series, so the value axis always includes 0 and is never log-scaled or clamped. Stacked bars compute it per category, with positive and negative sums separate and hidden series excluded.
  • The identity of a series is id ?? name. Two series or slices sharing an identity get distinct keys (a, a#1, ...), and in development mode the chart logs one warning for a duplicate explicit id and for a data array whose length differs from categories.
  • A flat domain (every value equal) is expanded so the axis never collapses, and ticks are rounded to the precision their step needs.
  • series[].color or slices[].color is optional; without it the marks use a round-robin over the eight --kui-chart-series-1..8 tokens in the order given.

The legend shows automatically when there is more than one series or slice; the legend input overrides it. Clicking an item hides that series or slice.

  • Hiding a series does not recompute the value-axis domain in grouped mode, so the scale stays stable. A stacked bar is the exception: hiding a series recomputes the domain and the remaining stack collapses to its own height.
  • Hovering a legend item highlights the matching mark, and hovering a mark highlights its legend entry. Hidden items stay interactive; "every series hidden" is a legend-only state and not the empty composition.
  • A hidden item is marked by more than its pressed state: the label is struck through and the swatch becomes an outline.

Standalone legend

Every chart implements KuiChartLegendSource: a public legendItems() and hoveredLegendId() read surface plus toggleLegendItem(id) and setHoveredLegendId(id). Pass the chart through a template reference variable to kui-chart-legend to render the legend anywhere, and set [legend]="false" on the chart so it is not drawn twice. Project a kuiChartLegendItem template (context KuiChartLegendItemContext: $implicit is the KuiChartLegendEntry, hovered the cross-highlight state) to replace the default button markup; wiring the toggle and hover calls is the template's job.

<kui-line-chart
  #chartRef
  ariaLabel="Sessions per day"
  [series]="series"
  [categories]="categories"
  [legend]="false"
/>
<kui-chart-legend [chart]="chartRef" />

size sets the plot height in CSS pixels; the chart follows the width of its container.

  • size is sm, md or lg (default md): a plot height of 200, 280 or 360px.
  • The chart measures its container with a ResizeObserver and draws at one user unit per CSS pixel, so text, strokes and marks keep their pixel size at any width. The axis padding fits the widest measured tick label and the axis title.
  • Server rendering and the first client render use a nominal width (320, 480 or 640px) inside a graphic whose height is already final, so hydration does not shift the layout.
  • Tick labels are thinned by their measured width plus an 8px gap so they never overlap, and a label wider than its slot is cut with an ellipsis (the full text stays in the element <title>). Axis text defaults to --kui-text-sm-size; the --kui-chart-axis-text-font-size token changes it.
  • Axis titles come from axes.xTitle and axes.yTitle and are drawn in the SVG. There is no built-in zoom, pan or decimation: aggregate very dense data before passing it in.

loading shows a skeleton shaped like the chart instead of a generic spinner. Missing data renders an empty composition.

  • The bar skeleton is a silhouette of bars with varying heights, and it shrinks with its container instead of overflowing it.
  • An empty or missing input (no non-gap value anywhere) always renders the empty composition: a dashed border, a faded chart icon and the "No data" text. It is not the kui-empty-state component.
  • The loading and empty texts are chart messages.

The tooltip text and the numbers on axes and in the legend can be replaced.

protected formatTooltip(point: KuiChartPoint): string {
  return `${point.categoryLabel}: $${point.value.toLocaleString('en-US')}`;
}
  • tooltip overrides the default text: <series> · <category>: <value>.
  • valueFormat defaults to the compact notation of the locale (1.2K and 3.4M in English, 1,5 Mio. in German). It formats axis ticks and the default tooltip and legend numbers, and is never used for the alt-table, which shows exact values.
  • The tooltip is dismissible (Escape closes it without moving focus or the pointer until a new trigger) and persistent (it stays until hover or focus ends). It follows the pointer and ignores pointer events, so it is not hoverable (WCAG 1.4.13); see Known gaps.
  • Accessible names, role descriptions, the loading and empty texts, the table headers and the point text are chart messages. Every chart takes a messages input for one-off overrides.

The Table button switches the same data to a table representation with exact values.

  • The alt-table is the accessible-charts recommendation of pairing a graphic with a tabular alternative, not a decorative duplicate. It lists the visible series with exact (not compact-formatted) numbers and has a caption with the chart name.
  • It lists every point of a dense chart whatever the chart draws.
  • The Table button keeps keyboard focus when the view switches.

Colour is never the only way to tell series apart (WCAG 1.4.1).

  • patterns (default false) fills each series with one of eight hatch patterns, a tinted ground with a hatch in the full series colour, and the legend swatch becomes a patterned square. Pattern ids are unique per chart instance, so many charts on one page never collide.
  • In forced colours (Windows High Contrast) the chart draws with the system colours CanvasText, Canvas, GrayText and Highlight only. Series then differ by hatch pattern, by dash (lines) and by marker shape; patterns and dashes turn on automatically.

Tab enters a chart at the last focused mark and leaves with Tab; focus follows the arrows.

  • Grouped bars: ArrowLeft and ArrowRight along the categories in the same series, ArrowUp and ArrowDown between series at the same category, Home and End the first and last category of the series.
  • Stacked bars: ArrowUp and ArrowDown move between the segments of a stack.
  • Horizontal bars swap the pairs: ArrowUp and ArrowDown go along the categories, ArrowLeft and ArrowRight between series.
  • Escape dismisses the tooltip; Enter and Space on a legend item toggle it.
  • The roving position is a key (series and category), not an index, so hiding a series moves it to the nearest remaining mark and re-focuses that mark if focus was inside the chart. Focus mirrors hover: the focused mark highlights its series and opens the tooltip.

A chart draws every shape it is given, up to a limit for line marks.

  • Bar charts draw every shape. The practical limit is a few thousand shapes; above that, aggregate before passing the data in.
  • Library measurements in Chromium (a two-series line chart and a one-series scatter chart of the same point count): 500 points paint in 46 ms with 1,629 DOM nodes, 2,000 points in 115 ms with 6,123 nodes and 5,000 points in 259 ms with 15,123 nodes. Hiding a series on the 2,000-point line chart paints in 27 ms. Decimation is out of scope.

Set defaults.barChart once for the application or for a subtree. Each option resolves as local input > defaults.barChart.<option> > built-in default.

import { provideKikitaUi } from '@kikita-labs/ui';

export const appConfig: ApplicationConfig = {
  providers: [
    provideKikitaUi({
      defaults: {
        barChart: {
          /* options below */
        },
      },
    }),
  ],
};
NameTypeDefaultDescription
size'sm' | 'md' | 'lg' — Chart size.
legendboolean — Shows the chart legend.

Set any of these on the component or an ancestor to restyle one part. Each token is optional: when it is not set, the part uses the role or scale token in the default column.

NameTypeDefaultDescription
--kui-chart-mark-strokeCSS custom property--kui-color-surfaceMark stroke
--kui-chart-pattern-ground-colorCSS custom property--kui-color-surfaceGround under hatch patterns
--kui-chart-axis-title-colorCSS custom property--kui-chart-axis-label-colorAxis title color
--kui-chart-empty-borderCSS custom property--kui-color-borderEmpty border color
--kui-chart-empty-colorCSS custom property--kui-color-text-secondaryEmpty color
--kui-chart-legend-item-bg-activeCSS custom property--kui-color-surface-sunkenLegend item background, active
--kui-chart-legend-item-color-activeCSS custom property--kui-color-textLegend item color, active
NameTypeDefaultDescription
--kui-chart-gapCSS custom property--kui-space-3Gap
--kui-chart-axis-text-font-sizeCSS custom property--kui-text-sm-sizeAxis text font size
--kui-chart-loading-bars-gapCSS custom property--kui-space-3Loading bars gap
--kui-chart-loading-bars-paddingCSS custom property--kui-space-4Loading bars padding
--kui-chart-empty-gapCSS custom property--kui-space-3Empty gap
--kui-chart-empty-radiusCSS custom property--kui-radius-mdEmpty corner radius
--kui-chart-empty-text-font-sizeCSS custom property--kui-text-sm-sizeEmpty text font size
--kui-chart-toolbar-gapCSS custom property--kui-space-3Toolbar gap
--kui-chart-legend-gapCSS custom property--kui-space-3Legend gap
--kui-chart-legend-item-gapCSS custom property--kui-space-2Legend item gap
--kui-chart-legend-item-radiusCSS custom property--kui-radius-smLegend item corner radius
--kui-chart-legend-item-padding-inlineCSS custom property--kui-space-1Legend item padding, inline
--kui-chart-legend-item-font-sizeCSS custom property--kui-text-sm-sizeLegend item font size
--kui-chart-legend-swatch-sizeCSS custom property12pxLegend swatch size

Every text the component owns (accessible names, visible words, announcements) is a typed message with an English default. Override it for the application with provideKikitaUi({ messages }), or for a subtree with provideKuiMessages.

NameTypeDefaultDescription
loadingstring — Accessible name of the loading state. Default Loading chart.
noDatastring — Text shown when there is no data. Default No data.
viewChartstring — Button that shows the chart again from its data table. Default Chart.
viewTablestring — Button that shows the data table instead of the chart. Default Table.
columnCategorystring — Header of the category column in the data table. Default Category.
columnSeriesstring — Header of the series column in the data table. Default Series.
columnLabelstring — Header of the label column in the data table. Default Label.
columnValuestring — Header of the value column in the data table. Default Value.
columnXstring — Header of the horizontal-axis column of a scatter chart's data table. Default X.
columnYstring — Header of the vertical-axis column of a scatter chart's data table. Default Y.
columnRadiusstring — Header of the radius column of a bubble chart's data table. Default Radius.
legendLabelstring — Accessible name of the chart's inline legend. Default Legend.
lineLabelstring — Accessible name of a line chart. Default Line chart.
barLabelstring — Accessible name of a bar chart. Default Bar chart.
donutLabelstring — Accessible name of a donut chart. Default Donut chart.
scatterLabelstring — Accessible name of a scatter chart. Default Scatter chart.
lineRoleDescriptionstring — Role description of a line chart. Default line chart.
barRoleDescriptionstring — Role description of a bar chart. Default bar chart.
donutRoleDescriptionstring — Role description of a donut chart. Default donut chart.
scatterRoleDescriptionstring — Role description of a scatter chart. Default scatter chart.
pointWithCategoryKuiMessageFn<{ series: string; category: string; value: string; }> — Tooltip and name of a mark that has a category. Default {series} · {category}: {value}.
pointKuiMessageFn<{ series: string; value: string; }> — Tooltip and name of a mark without a category. Default {series}: {value}.
scatterPointKuiMessageFn<{ series: string; x: string; y: string; }> — Tooltip and name of a scatter point. Default {series}: ({x}, {y}).
bubblePointKuiMessageFn<{ series: string; x: string; y: string; radius: string; }> — Tooltip and name of a bubble. Default {series}: ({x}, {y}), radius {radius}.
sliceKuiMessageFn<{ label: string; value: string; percent: number; }> — Tooltip and name of a donut slice. Default {label}: {value} ({percent}%).

Inputs verified against @kikita-labs/ui v2.0.0 public typings.

NameTypeDefaultDescription
seriesreadonly KuiChartCartesianSeries[]-Required. { id?, name, color?, data }, with data aligned index for index with categories. Without a non-gap value the empty composition renders.
categoriesreadonly string[][]Category labels aligned with each series data. The shorter side wins.
orientation'vertical' | 'horizontal''vertical'Flips the on-screen bar direction only; axes.x and axes.y stay semantic.
stackedbooleanfalseStacks the series within each category. Only meaningful with more than one series; positive and negative values stack separately. Hiding a series recomputes the domain.
patternsbooleanfalseFills each series with one of eight hatch patterns (colour independence).
axesKuiChartAxesOptions{}{ x?, y?, gridLines?, xTitle?, yTitle? }: show or hide an axis, choose the grid lines and set the axis titles drawn in the SVG.
size'sm' | 'md' | 'lg' | undefinedundefinedPlot height of 200, 280 or 360px (a square of up to that size for the donut). Falls back to the chart defaults key, then md.
loadingbooleanfalseShows a skeleton placeholder shaped like the chart instead of the chart.
legendboolean | undefinedundefinedShows the inline legend. Falls back to the chart defaults key, then true when there is more than one series or slice.
valueFormat(value: number) => stringcompact notationAxis tick, default tooltip and legend number formatting. The default is the compact notation of the locale; the alt-table always shows exact values.
tooltip(point: KuiChartPoint) => string | undefinedbuilt-in textOverrides the default tooltip and point label text.
ariaLabelstring | undefinedundefinedAccessible name of the chart as a whole. Falls back to the chart message for this type (Bar chart); prefer a content-specific name.
messagesPartial<KuiChartMessages> | undefinedundefinedPer-instance text overrides: names, role descriptions, loading and empty text, table headers and point text. They win over scoped and root messages.
legendItems() / hoveredLegendId()KuiChartLegendSource-Public read surface of the chart legend, used by kui-chart-legend.
toggleLegendItem(id) / setHoveredLegendId(id)KuiChartLegendSource-Hide or show a series or slice, and set the cross-highlighted legend item.
kui-chart-legend [chart]KuiChartLegendSource-Renders the legend of any chart through a template reference variable. Project a kuiChartLegendItem template (KuiChartLegendItemContext) to replace the buttons.
KuiBarChartOptions / KuiLineChartOptions / KuiScatterChartOptions / KuiDonutChartOptionsinterfaces-Shape of defaults.barChart, lineChart, scatterChart and donutChart: size and legend.

The chart is an SVG graphic with an accessible name, labelled marks and a table alternative.

  • The <svg> is the graphic: role="graphics-document" with the name and the role description (aria-roledescription). The wrapper has no role, and axis text, gridlines and decorative shapes are aria-hidden.
  • Each bar is role="graphics-symbol img" with its own aria-label, the same text as the tooltip, as an indivisible graphical unit per the WAI-ARIA Graphics Module.
  • ariaLabel has a chart-type fallback name from the chart messages and should describe what the chart shows. It is distinct from each mark's own label (its value).
  • The marks use a roving tabindex (see Keyboard). The inline legend is a named group of native <button aria-pressed> items at least 24px high.
  • One shared tooltip overlay is retargeted between marks, so hover and keyboard focus show the same text. On pointer hover it appears at the pointer and follows it; keyboard focus anchors it to the focused mark. On touch, a tap pins the tooltip until an outside tap, Escape or a tap on a different mark, and it always renders below 768px.
  • Pointer hit targets: the bar itself.
  • Text is at least 12px on screen; axis text defaults to 13px at any width.
  • prefers-reduced-motion: reduce disables the hover and dim transition and the donut sweep of sibling charts.
  • The alt-table is a real accessible alternative to the graphic.

The library checks keyboard, the accessibility tree, axe, forced-colours media emulation and reduced motion automatically. A session with a real screen reader has not been run and stays pending.

Documented limits of the installed package.

  • The chart tooltip follows the pointer and is not hoverable, so it does not meet the hoverable part of WCAG 1.4.13 (it is dismissible and persistent). A tooltip that is hoverable and follows the cursor would catch the cursor, so anchoring it to the mark is an open design decision.
  • A bar is a <path> and its size change (hiding a series on a stacked chart) is a CSS transition of d. Chromium animates it; Safari and Firefox are expected to jump to the new size.
  • There is no zoom, pan or decimation, and bar and scatter charts draw every shape (see Performance).
  • The marker shapes and hatch patterns are engineering decisions that the designer still has to confirm. A formal assistive-technology pass with a real screen reader and a real Windows forced-colours session is still pending.