Components · Charts
ChartFrame
The container every chart sits in: title, controls, legend, loading / empty / error states and an accessible data table.
Header controls, legend and table toggle
Requests
New requests per day
- Requests
| Day | Requests |
|---|---|
| Mon | 48 |
| Tue | 61 |
| Wed | 55 |
| Thu | 72 |
| Fri | 66 |
import { useState } from 'react';
import { TabsFilter } from '@rookcom/monarch-react';
import { ChartFrame, Sparkline, type ChartTableData } from '@rookcom/monarch-react/charts';
const WEEK = [
{ day: 'Mon', requests: 48 },
{ day: 'Tue', requests: 61 },
{ day: 'Wed', requests: 55 },
{ day: 'Thu', requests: 72 },
{ day: 'Fri', requests: 66 },
];
const TABLE: ChartTableData = {
columns: [
{ key: 'day', label: 'Day' },
{ key: 'requests', label: 'Requests' },
],
rows: WEEK.map((d) => ({ day: d.day, requests: d.requests })),
};
const RANGES = [
{ value: '7d', label: '7 days' },
{ value: '30d', label: '30 days' },
];
function FrameDemo() {
const [range, setRange] = useState('7d');
return (
<ChartFrame
title="Requests"
description="New requests per day"
headingLevel={3}
height={96}
actions={
<TabsFilter
aria-label="Range"
variant="pill"
options={RANGES}
value={range}
onValueChange={setRange}
/>
}
legend={[{ key: 'requests', label: 'Requests', color: 1 }]}
table={TABLE}
footer="Counts update every hour."
>
<Sparkline
values={WEEK.map((d) => d.requests)}
variant="area"
color={1}
height={96}
responsive
/>
</ChartFrame>
);
}
<FrameDemo />Usage examples
Loading, empty and error
import { ChartFrame } from '@rookcom/monarch-react/charts';
<ChartFrame title="Loading" status="loading" height={80} />
<ChartFrame
title="Empty"
status="empty"
height={80}
emptyDescription="Requests appear here after the first one arrives."
/>
<ChartFrame title="Error" status="error" height={80} onRetry={() => undefined} />Purpose
Gives every chart the same frame and the same accessibility: a labelled figure, a header with period or metric controls, a legend, consistent loading, empty and error states, and the data as a real table, visible on demand and always present for screen readers.
Use when
- Wrapping a custom visualisation that Monarch does not ship, so it still gets the header, states and table fallback.
- Building a new chart component: LineChart and BarChart render ChartFrame for you, and the other ./charts components build on it.
Avoid when
- A single headline number with no plot: use StatCard.
- A plain list of labelled values: use DescriptionList or BarList.
Design principles
- The data is never only a picture:
tableis always rendered for assistive technology and one click away for everyone else. - States keep the reserved
height, so loading, empty, error and ready swap without the page jumping. - Controls belong in the header (
actions), above the plot, in one row that wraps on narrow screens. - A legend only for two or more series; a single series is named by the title.
- Series colours are palette slots (
chartColor(1…5)→--mn-color-data-*), never status colours and never raw values.
API reference
import { ChartFrame, chartColor } from '@rookcom/monarch-react/charts';| Export | Kind | Rendering | Root class |
|---|---|---|---|
ChartFrame | component | client | .mn-chart-frame |
chartColor | utility | server-safe | – |
ChartFrame renders a <figure> (ref to it) labelled by title and described by description; other HTML attributes pass through.
chartColor(slot) returns the CSS colour for a palette slot (1–5 → var(--mn-color-data-N), neutral → var(--mn-color-text-subtle)), for custom marks drawn inside a frame.
Types shared by every chart are exported from here: ChartSeries, ChartDatum, ChartColor, ChartLegendItem, ChartTableData, ChartTableColumn.
Generated from the TypeScript declarations of packages/react/src/components/charts/chart-frame.
Properties
ChartFrame
The container every Monarch chart sits in: a `<figure>` with a title, description, controls slot, legend, loading / empty / error states and an accessible data table ("View as table", always present for screen readers).
| Prop | Type | Description |
|---|---|---|
title required | ReactNode | The chart's name. Labels the figure. Required. |
actions | ReactNode | Period or metric controls in the header, such as a pill TabsFilter or a Select. |
children | ReactNode | The chart. |
className | string | |
defaultShowTable | boolean | Uncontrolled initial table view. Default `false`. |
description | ReactNode | One line under the title: the period, unit or what the chart answers. Describes the figure. |
emptyActions | ReactNode | Actions in the empty state, such as a link to set up tracking. |
emptyDescription | ReactNode | Empty-state description, such as what will appear and when. |
footer | ReactNode | Notes under the chart: source, last updated, a footnote. |
headingLevel | ChartFrameHeadingLevel | Render the title as a heading (`h2`–`h6`) to match the page outline. Default: not a heading. |
height | number | Height reserved for the chart body (loading, empty and error keep it, so nothing jumps). Default 240. |
id | string | |
labels | ChartFrameLabels | |
legend | readonly ChartLegendItem[] | Legend entries, shown under the header while the chart is visible. |
legendMarker | ChartLegendMarker | Legend marker shape. Default `line`. |
onLegendToggle | ((key: string) => void) | Makes legend entries toggle buttons; called with the series key. |
onRetry | (() => unknown) | Error-state retry. Return the refetch promise to keep the button busy until it settles. |
onShowTableChange | ((showTable: boolean) => void) | |
showTable | boolean | Controlled table view. |
status | ChartFrameStatus | `ready` (default) shows the chart; `loading` a skeleton with `aria-busy`; `empty` an empty state; `error` an error with Try again when `onRetry` is set. Exposed as `data-status`. |
table | ChartTableData | The accessible data table: every value the chart shows, as text. It is always in the page for screen readers, and visible through the "View as table" toggle when `tableToggle` is on. |
tableToggle | boolean | Show the "View as table" toggle. Default `true` when `table` is set. |
Variants
| Prop | Values | Description |
|---|---|---|
status | 'ready''loading''empty''error' | Which body shows: the chart, a skeleton, an EmptyState or an error (InlineErrorRetry with onRetry, otherwise an alert). Exposed as data-status. |
headingLevel | '2''3''4''5''6' | Renders the title as that heading; without it the title is a plain block. |
legendMarker | 'line''square' | Short line (line charts) or square (bars) beside each legend label. |
tableToggle | 'true''false' | Shows the "View as table" button (default when table is set). Off, the table is still present for screen readers. |
States
| State | Exposed as | Behaviour |
|---|---|---|
| Ready | data-status="ready", data-view="chart" | The chart, plus the visually hidden table. |
| Table view | data-view="table" | The table replaces the chart in a focusable, scrollable region; the legend hides. |
| Loading | data-status="loading", aria-busy="true" | A skeleton of the reserved height and a polite "Loading chart" status. |
| Empty | data-status="empty" | A compact EmptyState with emptyDescription and emptyActions. |
| Error | data-status="error" | InlineErrorRetry with Try again when onRetry is set; otherwise a message with role="alert". |
| Hidden series | aria-pressed="false", data-hidden | A toggled-off legend entry: struck-through label and faded swatch. |
Interaction behaviour
- "View as table" swaps the chart for the table and changes to "View as chart"; controlled with
showTable+onShowTableChange, or uncontrolled withdefaultShowTable. - With
onLegendToggle, each legend entry is a toggle button; the chart decides what hiding means. - Try again calls
onRetry; return the refetch promise and the button stays busy until it settles.
Keyboard support
| Keys | Action |
|---|---|
| Tab | Moves through the header controls, the table toggle, legend toggles and the chart. |
| Enter / Space | Activates the table toggle or a legend toggle. |
| Arrow keys (table view) | Scroll the focused table region when it overflows. |
Accessibility
- Role
- figure
- Accessible name
title(aria-labelledby);descriptionis the accessible description.
- The data table has a caption (the title unless
table.captionis set), column headers withscope="col"and row headers withscope="row". - While the chart shows, the table sits in a visually hidden container, so screen readers always reach the values.
- The visible table is a focusable region (
tabindex="0"), so keyboard users can scroll it. - Legend toggles are buttons with
aria-pressed; the legend list is named "Legend" (labels.legend). - Loading sets
aria-busyand announces "Loading chart"; a retry-less error isrole="alert". - Identity is never colour alone: every legend entry has its label, dashed series show a dashed marker.
- All text is translatable through
labels.
Responsive behaviour
- The header wraps: actions move under the title when there is no room.
- Padding tightens below 30rem; the frame works at 320px.
- Legend toggles grow to a 44px touch target on coarse pointers.
Theme support
- Surface, border and text use Monarch tokens and follow light, dark and high-contrast themes.
- Series colours are
--mn-color-data-1…-5, chosen and contrast-checked per theme in @rookcom/monarch-tokens (3:1 on every chart background, 4.5:1 in high contrast). - Focus rings use
--mn-color-focus-ring.
Do and don't
Do
- Pass
tablewith every value the chart shows, formatted as the chart formats it. - Reserve a
heightclose to the real chart so states do not shift the layout. - Put period and metric switches in
actions.
Don't
- Hide the only text alternative: never set
tableToggle={false}and leave outtable. - Colour series with status tokens or hex values; use
chartColorslots. - Put a legend on a single-series chart.
Related components
Dependencies
| Kind | Depends on |
|---|---|
| npm packages | react |
| Monarch components | Button, EmptyState, InlineErrorRetry, Skeleton |
Shared chart internals (legend, table, palette) come from src/internal/charts; their styles are styles/chart.css (.mn-chart__*).
Testing coverage
- Figure named by the title and described by the description; no axe violations.
- The data table is always present (visually hidden) with row and column headers.
- The toggle swaps chart and table, uncontrolled and controlled; the visible table is a focusable region.
tableToggle={false}removes the button but keeps the hidden table.- Heading level and actions slot render.
- Loading (aria-busy, status text), empty (description, actions) and error (Try again calls onRetry; alert without it; translated labels).
- Legend toggles expose aria-pressed and report the key; a static legend has no buttons.
chartColormaps slots to data tokens only.- Renders on the server.
| Check | Covered |
|---|---|
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery/chart-frame; Cross-browser axe: /components/chart-frame |
Tests: packages/react/src/components/charts/chart-frame