Combo Chart
- Stable
- WCAG 2.2 evidence
- RTL
A combo chart: bars and lines over the same categories, optionally on two value axes (a cost in
the thousands next to a headcount in the hundreds). Data driven: categories and series in, SVG out, rendered on the server from
the same props. Accessibility: the figure is named by title, described by summary and backed by a data table with every series in its own
formatted units (WCAG 1.1.1, 1.3.1). The plot is ONE tab stop; Left and Right, Home, End, Page Up and Page Down move between
categories, each announced with all bar and line values; the tooltip shows on hover and focus, stays while hovered and closes with
Escape (2.1.1, 1.4.13). Bars and lines differ by shape, colour, dash and marker and the legend toggles them (1.4.1). The draw-in is
behind motion-safe:. Labels rotate or thin out at narrow widths (1.4.10); the plot stays left-to-right in right-to-left pages.
With two axes, say which axis a series uses in the series name, the axis titles and the summary: the table lists each value in
its own unit.
import { ComboChart } from "@nexera-ui/react";- 11
- examples
- 26
- props
- 5
- live controls
- 1
- platform
- 9
- WCAG criteria
- 1
- block uses it
Try every prop. Copy the code.
Change the props and the code updates. Check light and dark, LTR and RTL, and phone width.
import { ComboChart } from "@nexera-ui/react";
<ComboChart />Examples 10
The same examples as Storybook, rendered live. Open Code to copy one.
Single axis
Without secondaryAxis bars and line share one axis (here a plan line over the bars).
import { ComboChart, IconButton } from "@nexera-ui/react";
import { LuEllipsis } from "react-icons/lu";
const MONTHS = ["Nov", "Dec", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct"];
const menu = <IconButton aria-label="More options" variant="ghost" icon={<LuEllipsis />} />;
export function SingleAxis() {
return (
<ComboChart
title="Hires against plan"
description="Per month"
summary="Hires stayed close to the plan of 6 a month."
categories={MONTHS}
series={[
{ name: "Hires", type: "bar", data: [4, 3, 7, 5, 6, 8, 5, 9, 7, 6, 4, 8] },
{ name: "Plan", type: "line", data: MONTHS.map(() => 6), styleIndex: 7 },
]}
actions={menu}
/>
);
}
Smooth
Smooth monotone curve for the line.
import { ComboChart, IconButton } from "@nexera-ui/react";
import { LuEllipsis } from "react-icons/lu";
const menu = <IconButton aria-label="More options" variant="ghost" icon={<LuEllipsis />} />;
<ComboChart actions={menu} smooth />;
Range switch
import { useState } from "react";
import { ComboChart, IconButton } from "@nexera-ui/react";
import { LuEllipsis } from "react-icons/lu";
const MONTHS = ["Nov", "Dec", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct"];
const COST = [
18_600_000, 19_000_000, 19_800_000, 20_300_000, 20_700_000, 21_500_000, 22_300_000, 22_900_000,
23_400_000, 24_100_000, 24_600_000, 25_400_000,
];
const HEADCOUNT = [96, 99, 103, 101, 106, 110, 108, 114, 117, 121, 125, 128];
const menu = <IconButton aria-label="More options" variant="ghost" icon={<LuEllipsis />} />;
export function RangeSwitch() {
const [selected, setSelected] = useState("6m");
const count = selected === "3m" ? 3 : selected === "6m" ? 6 : 12;
return (
<ComboChart
actions={menu}
categories={MONTHS.slice(-count)}
series={[
{ name: "Cost", type: "bar", data: COST.slice(-count) },
{ name: "Headcount", type: "line", data: HEADCOUNT.slice(-count), styleIndex: 2 },
]}
range={{
"aria-label": "Range",
options: [
{ id: "3m", label: "3M" },
{ id: "6m", label: "6M" },
{ id: "12m", label: "12M" },
],
value: selected,
onChange: setSelected,
}}
/>
);
}
Loading
import { ComboChart, IconButton } from "@nexera-ui/react";
import { LuEllipsis } from "react-icons/lu";
const menu = <IconButton aria-label="More options" variant="ghost" icon={<LuEllipsis />} />;
<ComboChart actions={menu} state={{ status: "loading" }} />;
Empty
import { ComboChart, IconButton, type ComboChartProps } from "@nexera-ui/react";
import { LuEllipsis } from "react-icons/lu";
const MONTHS = ["Nov", "Dec", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct"];
const COST = [
18_600_000, 19_000_000, 19_800_000, 20_300_000, 20_700_000, 21_500_000, 22_300_000, 22_900_000,
23_400_000, 24_100_000, 24_600_000, 25_400_000,
];
const HEADCOUNT = [96, 99, 103, 101, 106, 110, 108, 114, 117, 121, 125, 128];
const million = (value: number): string => `Rs ${(value / 1_000_000).toFixed(1)}M`;
const payroll = {
title: "Payroll cost and headcount",
description: "Monthly cost (left axis) and people (right axis)",
summary:
"Monthly payroll cost rose from Rs 18.6M to Rs 25.4M while headcount grew from 96 to 128 people.",
categories: MONTHS,
series: [
{ name: "Cost", type: "bar", data: COST },
{ name: "Headcount", type: "line", data: HEADCOUNT, styleIndex: 2 },
],
primaryAxis: {
label: "Cost (Rs)",
formatValue: million,
formatAxisValue: (value: number) => `Rs ${String(value / 1_000_000)}M`,
},
secondaryAxis: { label: "People" },
} satisfies ComboChartProps;
const menu = <IconButton aria-label="More options" variant="ghost" icon={<LuEllipsis />} />;
<ComboChart
actions={menu}
categories={[]}
series={[{ name: "Cost", type: "bar", data: [] }]}
state={{ status: "empty", title: "No payroll data", description: "Pick another period." }}
/>;
Error state
import { Button, ComboChart, IconButton, type ComboChartProps } from "@nexera-ui/react";
import { LuEllipsis } from "react-icons/lu";
const MONTHS = ["Nov", "Dec", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct"];
const COST = [
18_600_000, 19_000_000, 19_800_000, 20_300_000, 20_700_000, 21_500_000, 22_300_000, 22_900_000,
23_400_000, 24_100_000, 24_600_000, 25_400_000,
];
const HEADCOUNT = [96, 99, 103, 101, 106, 110, 108, 114, 117, 121, 125, 128];
const million = (value: number): string => `Rs ${(value / 1_000_000).toFixed(1)}M`;
const payroll = {
title: "Payroll cost and headcount",
description: "Monthly cost (left axis) and people (right axis)",
summary:
"Monthly payroll cost rose from Rs 18.6M to Rs 25.4M while headcount grew from 96 to 128 people.",
categories: MONTHS,
series: [
{ name: "Cost", type: "bar", data: COST },
{ name: "Headcount", type: "line", data: HEADCOUNT, styleIndex: 2 },
],
primaryAxis: {
label: "Cost (Rs)",
formatValue: million,
formatAxisValue: (value: number) => `Rs ${String(value / 1_000_000)}M`,
},
secondaryAxis: { label: "People" },
} satisfies ComboChartProps;
const menu = <IconButton aria-label="More options" variant="ghost" icon={<LuEllipsis />} />;
<ComboChart
actions={menu}
state={{
status: "error",
title: "Couldn't load payroll",
description: "Check your connection and try again.",
action: (
<Button variant="secondary" size="sm">
Try again
</Button>
),
}}
/>;
Single point
One category: a single bar and a marked line point.
import { ComboChart, IconButton, type ComboChartProps } from "@nexera-ui/react";
import { LuEllipsis } from "react-icons/lu";
const MONTHS = ["Nov", "Dec", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct"];
const COST = [
18_600_000, 19_000_000, 19_800_000, 20_300_000, 20_700_000, 21_500_000, 22_300_000, 22_900_000,
23_400_000, 24_100_000, 24_600_000, 25_400_000,
];
const HEADCOUNT = [96, 99, 103, 101, 106, 110, 108, 114, 117, 121, 125, 128];
const million = (value: number): string => `Rs ${(value / 1_000_000).toFixed(1)}M`;
const payroll = {
title: "Payroll cost and headcount",
description: "Monthly cost (left axis) and people (right axis)",
summary:
"Monthly payroll cost rose from Rs 18.6M to Rs 25.4M while headcount grew from 96 to 128 people.",
categories: MONTHS,
series: [
{ name: "Cost", type: "bar", data: COST },
{ name: "Headcount", type: "line", data: HEADCOUNT, styleIndex: 2 },
],
primaryAxis: {
label: "Cost (Rs)",
formatValue: million,
formatAxisValue: (value: number) => `Rs ${String(value / 1_000_000)}M`,
},
secondaryAxis: { label: "People" },
} satisfies ComboChartProps;
const menu = <IconButton aria-label="More options" variant="ghost" icon={<LuEllipsis />} />;
<ComboChart
actions={menu}
title="Payroll in October"
description="One month"
summary="October payroll was Rs 25.4M for 128 people."
categories={["Oct"]}
series={[
{ name: "Cost", type: "bar", data: [25_400_000] },
{ name: "Headcount", type: "line", data: [128], styleIndex: 2 },
]}
/>;
Many points
240 points: labels thin out to what fits and the keys still reach every day.
import { ComboChart, IconButton } from "@nexera-ui/react";
import { LuEllipsis } from "react-icons/lu";
const menu = <IconButton aria-label="More options" variant="ghost" icon={<LuEllipsis />} />;
const DAYS = Array.from({ length: 240 }, (_, i) => `Day ${String(i + 1)}`);
<ComboChart
actions={menu}
title="Orders and average basket"
description="Per day"
summary="Orders and the average basket move together."
categories={DAYS}
series={[
{ name: "Orders", type: "bar", data: DAYS.map((_, i) => 40 + ((i * 11) % 31)) },
{
name: "Average basket",
type: "line",
data: DAYS.map((_, i) => 55 + 10 * Math.sin(i / 20)),
styleIndex: 2,
},
]}
primaryAxis={{ label: "Orders" }}
secondaryAxis={{
label: "Average basket ($)",
valueFormat: { style: "currency", currency: "USD" },
}}
/>;
Long labels
Long titles, axis titles and series names wrap; category labels rotate; the table and tooltip keep the full names.
import { ComboChart, IconButton, type ComboChartProps } from "@nexera-ui/react";
import { LuEllipsis } from "react-icons/lu";
const MONTHS = ["Nov", "Dec", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct"];
const COST = [
18_600_000, 19_000_000, 19_800_000, 20_300_000, 20_700_000, 21_500_000, 22_300_000, 22_900_000,
23_400_000, 24_100_000, 24_600_000, 25_400_000,
];
const HEADCOUNT = [96, 99, 103, 101, 106, 110, 108, 114, 117, 121, 125, 128];
const million = (value: number): string => `Rs ${(value / 1_000_000).toFixed(1)}M`;
const payroll = {
title: "Payroll cost and headcount",
description: "Monthly cost (left axis) and people (right axis)",
summary:
"Monthly payroll cost rose from Rs 18.6M to Rs 25.4M while headcount grew from 96 to 128 people.",
categories: MONTHS,
series: [
{ name: "Cost", type: "bar", data: COST },
{ name: "Headcount", type: "line", data: HEADCOUNT, styleIndex: 2 },
],
primaryAxis: {
label: "Cost (Rs)",
formatValue: million,
formatAxisValue: (value: number) => `Rs ${String(value / 1_000_000)}M`,
},
secondaryAxis: { label: "People" },
} satisfies ComboChartProps;
const menu = <IconButton aria-label="More options" variant="ghost" icon={<LuEllipsis />} />;
<ComboChart
actions={menu}
title="Payroll cost and headcount across all departments, locations and employment contracts"
description="Monthly cost of salaries, benefits and social charges (left axis) and people on payroll (right axis)"
summary="Cost and headcount grew together."
categories={[
"Quarter four of 2025",
"Quarter one of 2026",
"Quarter two of 2026",
"Quarter three of 2026",
]}
series={[
{
name: "Total cost of salaries, benefits and social charges",
type: "bar",
data: [60_000_000, 62_000_000, 66_000_000, 70_000_000],
},
{
name: "Employees on payroll at the end of the quarter",
type: "line",
data: [96, 104, 112, 121],
styleIndex: 2,
},
]}
primaryAxis={{ label: "Cost", formatAxisValue: (value) => `Rs ${String(value / 1_000_000)}M` }}
secondaryAxis={{ label: "People" }}
/>;
Right to left
Right-to-left page: card, legend and table mirror, the plot keeps its left-to-right axes, digits follow the locale.
import { ComboChart, IconButton } from "@nexera-ui/react";
import { LuEllipsis } from "react-icons/lu";
const COST = [
18_600_000, 19_000_000, 19_800_000, 20_300_000, 20_700_000, 21_500_000, 22_300_000, 22_900_000,
23_400_000, 24_100_000, 24_600_000, 25_400_000,
];
const HEADCOUNT = [96, 99, 103, 101, 106, 110, 108, 114, 117, 121, 125, 128];
const menu = <IconButton aria-label="More options" variant="ghost" icon={<LuEllipsis />} />;
<ComboChart
actions={menu}
title="تكلفة الرواتب وعدد الموظفين"
description="التكلفة الشهرية (المحور الأيسر) والموظفون (المحور الأيمن)"
summary="ارتفعت تكلفة الرواتب مع زيادة عدد الموظفين."
categories={["نوف", "ديس", "يناير", "فبراير", "مارس", "أبريل"]}
series={[
{ name: "التكلفة", type: "bar", data: COST.slice(0, 6) },
{ name: "الموظفون", type: "line", data: HEADCOUNT.slice(0, 6), styleIndex: 2 },
]}
primaryAxis={{ label: "التكلفة" }}
secondaryAxis={{ label: "الموظفون" }}
table={{ categoryLabel: "الشهر", label: "عرض البيانات كجدول" }}
/>;
Props 26
Press "Try it" on a card to load that prop into the playground.
26 props shown
categories*Nexerareadonly string[]The x labels, one per data point ("Nov", "Dec",...). The first value of every series belongs to the first category.
categoryTitlesNexerareadonly string[]The long form of each category for the tooltip, the point labels and the table.
series*Nexerareadonly ComboSeries[]The bars and lines, with the axis each is measured on. The legend toggles them.
primaryAxisNexeraComboAxisOptionsThe left axis: its title, range and number format. The chart-level valueFormat, formatValue and formatAxisValue are the
default format of both axes.
secondaryAxisNexeraComboAxisOptionsThe right axis for the series set to axis: "secondary". Its ticks sit on the grid
lines of the left axis. Without it every series shares the left axis. Give it its own format: the two axes have different
units, so it does not inherit the left axis' format (the chart-level format is the default of both).
smoothNexerabooleanSmooth monotone curves for the lines.
patternsNexerabooleanDraw a fill pattern over every bar series except the first when there are several, in addition to the colour (WCAG 1.4.1).
title*NexerastringChart title: the question the chart answers ("Headcount"). Required: it names the figure, the plot and the data table (WCAG 1.1.1).
descriptionNexeraReactNodeSecondary line under the title.
summary*NexerastringOne or two sentences with what the chart shows and its main message ("Headcount grew from 96 to 128 over twelve months"). Required: it describes the figure for assistive technology (WCAG 1.1.1); the data table has the details.
rangeNexeraChartRangePropsRange switch in the header: 3M, 6M, 12M. Pass the data of the chosen range back in.
actionsNexeraReactNodeExtra controls in the header, after the range switch.
kpiNexeraChartKpiPropsKPI row above the plot: the headline number with an optional label or change badge.
stateNexeraChartPlotStateThe state that replaces the plot: loading (skeleton), empty or error. A chart without a value to plot shows the empty state
by itself; pass { status: "empty", title, description, action } to give it your wording.
tableNexeraChartTableOptionsThe data table that is the text alternative: caption, first-column header and whether it is visible or only for assistive technology.
localeNexerastringLocale of digits and separators.
valueFormatNexeraNumberFormatOptionsIntl.NumberFormat options for values ({ style: "currency", currency: "PKR" }, { style: "percent" }). Serialisable, so it
works from a Server Component; formatValue needs a Client Component.
formatValueNexera(value: number) => stringFormats a value for tooltips, point labels and the table (full precision). Wins over valueFormat.
formatAxisValueNexera(value: number) => stringFormats an axis tick (short: "29M").
heightNexeranumberHeight of the plot in px. The card grows with the header, KPI and legend around it.
isLegendHiddenNexerabooleanHide the legend. The series stay named in the tooltip, the point labels and the table.
plotLabelNexerastringAccessible name of the plot group. Translate it.
noDataLabelNexerastringText for a missing value in the tooltip and the table. Translate it.
hiddenSeriesNexerareadonly string[]Names of the hidden series (controlled). Pair it with onHiddenSeriesChange.
defaultHiddenSeriesNexerareadonly string[]Names of the series hidden at first (uncontrolled).
onHiddenSeriesChangeNexera(hidden: readonly string[]) => voidCalled with the names of the hidden series when the user toggles a legend entry.
* Required. React Aria props shown are the ones most apps use; the component accepts the rest of its React Aria props too.
Accessible by default.
Built on React Aria, and covered by the WCAG 2.2 evidence generated on every build.
WCAG 2.2 evidence
7 direct · 2 supporting- 1.1.1Non-text ContentLevel A · tested directly
- 1.3.1Info and RelationshipsLevel A · tested directly
- 1.4.1Use of ColorLevel A · supporting test
- 1.4.10ReflowLevel AA · tested directly
- 1.4.13Content on Hover or FocusLevel AA · tested directly
- 2.1.1KeyboardLevel A · tested directly
- 2.3.3Animation from InteractionsLevel AAA · supporting test
- 4.1.2Name, Role, ValueLevel A · tested directly
- 4.1.3Status MessagesLevel AA · tested directly
Fits any width.
Nexera components respond to the space they are given. Drag the corner of the frame, or pick a width.
Styling hooks
Pass className to add Tailwind classes (merged last). State is exposed as data attributes, so you can style it with variants like data-pressed:.
<ComboChart className="shadow-sm" />Used in blocks
Related components
- AccordionItemOne collapsible section of an FAQ or settings page.
- AreaChartA stacked area chart: layers piled up over time with the title, range switch, actions and legend of the card.
- AvailabilityRowOne person's availability across a window of the day: an avatar with initials and the name, and a timeline with the busy periods drawn as bars.
- AvatarA person or organisation shown as a photo, initials or an icon.
- AvatarGroupA row of overlapping avatars with a "+N" chip for the rest.
- AvatarLabelAn avatar with a name and an optional subtitle beside it, for a person in a list, a header or a mention.
- BadgeShort status or category label: `Tone` x `Variant` x `Size`, with an optional dot, leading icon and remove button.
- BadgeGroupA pill that pairs a small badge with a short message, for an announcement or a status line.