Component
chart
dx components add chartA themed, data-driven SVG chart (area, bar, line) with tooltip, legend, and an accessible hidden data table.
| Date | Desktop | Mobile |
|---|---|---|
| Apr 2 | 236 | 190 |
| Apr 3 | 250 | 191 |
| Apr 4 | 264 | 189 |
| Apr 5 | 277 | 186 |
| Apr 6 | 289 | 181 |
| Apr 7 | 301 | 174 |
| Apr 8 | 312 | 165 |
| Apr 9 | 322 | 156 |
| Apr 10 | 330 | 145 |
| Apr 11 | 338 | 134 |
| Apr 12 | 344 | 122 |
| Apr 13 | 349 | 111 |
| Apr 14 | 353 | 100 |
| Apr 15 | 355 | 90 |
| Apr 16 | 355 | 80 |
| Apr 17 | 354 | 73 |
| Apr 18 | 352 | 66 |
| Apr 19 | 348 | 62 |
| Apr 20 | 343 | 59 |
| Apr 21 | 337 | 58 |
| Apr 22 | 330 | 59 |
| Apr 23 | 321 | 63 |
| Apr 24 | 312 | 68 |
| Apr 25 | 301 | 75 |
| Apr 26 | 290 | 83 |
| Apr 27 | 279 | 93 |
| Apr 28 | 267 | 104 |
| Apr 29 | 255 | 116 |
| Apr 30 | 242 | 128 |
| May 1 | 230 | 140 |
| May 2 | 218 | 152 |
| May 3 | 206 | 164 |
| May 4 | 195 | 174 |
| May 5 | 185 | 184 |
| May 6 | 175 | 192 |
| May 7 | 167 | 198 |
| May 8 | 159 | 202 |
| May 9 | 153 | 205 |
| May 10 | 148 | 205 |
| May 11 | 144 | 204 |
| May 12 | 141 | 200 |
| May 13 | 140 | 195 |
| May 14 | 141 | 188 |
| May 15 | 143 | 180 |
| May 16 | 146 | 170 |
| May 17 | 151 | 159 |
| May 18 | 157 | 148 |
| May 19 | 164 | 137 |
| May 20 | 173 | 125 |
| May 21 | 182 | 114 |
| May 22 | 193 | 104 |
| May 23 | 205 | 95 |
| May 24 | 217 | 87 |
| May 25 | 230 | 81 |
| May 26 | 244 | 76 |
| May 27 | 258 | 73 |
| May 28 | 272 | 73 |
| May 29 | 286 | 74 |
| May 30 | 300 | 77 |
| May 31 | 313 | 83 |
| Jun 1 | 327 | 90 |
| Jun 2 | 339 | 98 |
| Jun 3 | 351 | 108 |
| Jun 4 | 362 | 119 |
| Jun 5 | 372 | 131 |
| Jun 6 | 381 | 143 |
| Jun 7 | 389 | 155 |
| Jun 8 | 395 | 167 |
| Jun 9 | 400 | 179 |
| Jun 10 | 404 | 189 |
| Jun 11 | 406 | 199 |
| Jun 12 | 407 | 207 |
| Jun 13 | 406 | 213 |
| Jun 14 | 404 | 217 |
| Jun 15 | 400 | 220 |
| Jun 16 | 395 | 220 |
| Jun 17 | 389 | 219 |
| Jun 18 | 382 | 215 |
| Jun 19 | 374 | 210 |
| Jun 20 | 364 | 203 |
| Jun 21 | 354 | 194 |
| Jun 22 | 343 | 184 |
| Jun 23 | 332 | 174 |
| Jun 24 | 320 | 163 |
| Jun 25 | 307 | 151 |
| Jun 26 | 295 | 140 |
| Jun 27 | 283 | 129 |
| Jun 28 | 271 | 118 |
| Jun 29 | 259 | 109 |
| Jun 30 | 248 | 102 |
- Desktop
- Mobile
Installation
Use the CLI command for the common path, or copy the component files manually.
Manual installation files
Copy the component source and CSS into your app. Import the shared theme CSS once near your app root.
use dioxus::prelude::*;
use dioxus_primitives::chart::{self, ChartContainerProps, ChartLegendProps, ChartProps, ChartTooltipProps};
use dioxus_primitives::dioxus_attributes::attributes;
use dioxus_primitives::merge_attributes;
// Plain, render-nothing value/data types a caller configures a chart with
// (dev-docs/preview-composition.md's allowlist rationale: "plain value
// types/enums that render nothing themselves have no theme for a wrapper to
// attach"). Re-exported here -- rather than left for demo code to import
// raw from `dioxus_primitives::chart` -- so every demo composes exclusively
// through `crate::components::chart::*`, never a raw `dioxus_primitives::`
// path outside this file (`scripts/check-preview-composition.sh`).
pub use dioxus_primitives::chart::{ChartConfig, ChartDatum, ChartKind, LegendAlign};
/// The themed chart container: scopes the `--color-<key>` CSS variables
/// generated from `config` to this instance via `data-chart="<id>"`. Always
/// the outermost chart piece -- `Chart`/`ChartTooltip`/`ChartLegend` are
/// composed as its children. The focusable/keyboard-navigable root
/// (`Chart`'s `keyboard` prop) is `Chart`'s own `[data-slot="chart"]` div,
/// not this container's -- see `Chart`'s doc comment below (`$S/chart-api.md`
/// "API changes" #4: an `Element`-typed child has no way to attach
/// attributes to its already-rendered parent without a post-mount effect,
/// which would make them absent from the first SSR render).
#[component]
pub fn ChartContainer(props: ChartContainerProps) -> Element {
let base = attributes!(div {
class: "dx-chart",
});
let merged = merge_attributes(vec![base, props.attributes]);
rsx! {
document::Link { rel: "stylesheet", href: asset!("/src/components/chart/style.css") }
chart::ChartContainer {
id: props.id,
config: props.config,
data: props.data,
kind: props.kind,
attributes: merged,
{props.children}
}
}
}
/// The chart's own visual: axes, grid, one mark per configured series (area
/// fill, bar, or line, per the container's `kind`), the hover hit-bands, and
/// the visually-hidden data table that carries the same series/category/
/// value data for assistive tech. Must be rendered inside a `ChartContainer`
/// -- reads its `kind`/`config`/`data` from that context, not from its own
/// props. Renders its own top-level `div[data-slot="chart"]` (wrapping the
/// svg + hidden table) that carries `tabindex`/`role="group"`/
/// `aria-roledescription="chart"`/`aria-label` and the keyboard handler when
/// `keyboard` is on -- `$S/chart-api.md`'s "API changes" #4. No base class
/// of its own: every mark (including this wrapper div) is unstyled by the
/// primitive and selected off `[data-slot="..."]` inside the container's
/// own `.dx-chart` scope (see `style.css`), so there is nothing new to
/// attach here beyond forwarding attributes through untouched.
#[component]
pub fn Chart(props: ChartProps) -> Element {
rsx! {
document::Link { rel: "stylesheet", href: asset!("/src/components/chart/style.css") }
chart::Chart {
aria_label: props.aria_label,
description: props.description,
width: props.width,
height: props.height,
stacked: props.stacked,
curve: props.curve,
show_grid: props.show_grid,
show_x_axis: props.show_x_axis,
show_y_axis: props.show_y_axis,
x_label: props.x_label,
x_tick_format: props.x_tick_format,
max_x_ticks: props.max_x_ticks,
y_tick_count: props.y_tick_count,
show_dots: props.show_dots,
keyboard: props.keyboard,
dir: props.dir,
attributes: props.attributes,
}
}
}
/// The hover tooltip: one row per series at the active data point, plus a
/// `role="graphics-symbol"` swatch per row. Rendered even when closed
/// (`data-state="closed"`, hidden by CSS) so there is nothing to attach
/// post-hydration. Optional -- omit it for a chart that doesn't need one.
#[component]
pub fn ChartTooltip(props: ChartTooltipProps) -> Element {
let base = attributes!(div {
class: "dx-chart-tooltip",
});
let merged = merge_attributes(vec![base, props.attributes]);
rsx! {
document::Link { rel: "stylesheet", href: asset!("/src/components/chart/style.css") }
chart::ChartTooltip {
label_format: props.label_format,
value_format: props.value_format,
hide_label: props.hide_label,
hide_indicator: props.hide_indicator,
attributes: merged,
// A named field, not the trailing `{props.children}` brace
// sugar: that sugar always populates the inner component's
// `children` with `Some(..)` (something was syntactically
// written in the child-node position, even if the expression
// itself evaluates to `None`), so every demo's plain
// `ChartTooltip {}` -- providing no custom content -- was
// tripping the primitive's "custom children replace the
// default" branch and rendering nothing at all. Assigning the
// `Option<Element>` value directly to the named field forwards
// it unchanged, so `None` reaches the primitive as `None`.
children: props.children,
}
}
}
/// The legend: one `role="graphics-symbol"` swatch + label per configured
/// series, in `config` order. Optional -- a single/dual-series chart
/// usually doesn't need one.
#[component]
pub fn ChartLegend(props: ChartLegendProps) -> Element {
let base = attributes!(ul {
class: "dx-chart-legend",
});
let merged = merge_attributes(vec![base, props.attributes]);
rsx! {
document::Link { rel: "stylesheet", href: asset!("/src/components/chart/style.css") }
chart::ChartLegend {
vertical_align: props.vertical_align,
attributes: merged,
}
}
}Usage notes
Chart is a themed, data-driven SVG chart engine (area, bar, line) built entirely from house
primitives β no third-party charting dependency, no injected/opaque markup. It mirrors shadcn's
own ChartConfig β CSS-variable theming idea, adapted to Dioxus's data-driven (not
children-as-configuration) component model: the series/axis data is a prop, not something a
parent introspects from nested children.
Component structure
// ChartConfig maps a series key to its label and color. Order matters -- it
// is also the legend/tooltip row order. `color` can be any CSS color or a
// `var(--token)` reference; the theme ships `--dx-chart-1..8` for this.
let config = ChartConfig::new()
.series("desktop", "Desktop", "var(--dx-chart-1)")
.series("mobile", "Mobile", "var(--dx-chart-2)");
// One ChartDatum per x-axis category. `values` has one entry per series, in
// the same order as `config` -- `None` renders as a gap (line/area) or a
// zero-height bar, never a fabricated zero.
let data = vec![
ChartDatum { label: "January".into(), values: vec![Some(186.0), Some(80.0)] },
ChartDatum { label: "February".into(), values: vec![Some(305.0), Some(200.0)] },
// ...
];
ChartContainer {
// Scopes the generated `--color-<key>` CSS variables to this instance
// via `data-chart="<id>"`. Auto-generated if omitted.
config,
data,
kind: ChartKind::Area, // Area | Bar | Line
// The chart itself: axes, grid, marks, the hidden data table, and the
// hover hit-bands. `aria_label` is required -- it is the chart's
// accessible name.
Chart {
aria_label: "Visitors by month, desktop and mobile",
stacked: false,
show_dots: false, // Line only
x_label: "Month", // hidden table's corner <th> -- default "Category"
max_x_ticks: 12, // thin x-axis labels on a dense chart -- see below
}
// Optional: the hover tooltip. Rendered even when closed (CSS hides
// it) so hydration never has to attach it after the fact.
ChartTooltip {}
// Optional: a legend with one swatch per configured series.
ChartLegend {}
}Chart, ChartTooltip, and ChartLegend are independent, explicitly-rendered pieces (matching
shadcn's own <ChartTooltip content={<ChartTooltipContent />} /> idiom) β omit whichever your
chart doesn't need, or a single/dual-series chart that doesn't need a legend at all.
Colors
Every series color is applied as a real CSS custom property (--color-<key>), scoped by
data-chart="<id>" and generated once by ChartContainer from its config prop β never inlined
per-mark. A series' color field can be a literal CSS color or, more usefully, one of this
theme's own categorical tokens (--dx-chart-1 through --dx-chart-8), which are already tuned
for both light and dark mode and for colorblind-safe adjacent contrast.
Accessibility contract
Chart does not use role="application" anywhere β that ARIA escape hatch hands every keystroke to
the widget and strips a screen-reader user of ordinary browse-mode navigation, and neither the APG
nor Radix defines a chart pattern to justify it. Instead:
- The SVG root carries
role="img", a required non-emptyaria-label(Chart'saria_labelprop), and a<title>/optional<desc>β a single, indivisible graphic, per the WAI-ARIA Graphics Module 1.0's own definition of that role. - A real, visually-hidden
<table>mirrors the exact same series/category/value data as the chart's actual screen-reader-facing path β one row per data point, one column per series, natively and correctly keyboard-navigable with zero bespoke widget behavior to get wrong. Its corner cell (<th scope="col">) carriesChart'sx_labelprop (default"Category") rather than being left empty β an empty<th>has no accessible name and fails axe'sempty-table-headerrule, since a screen-reader user browsing by column has no way to tell what the first column represents. Set it to whatever the x-axis actually is ("Date","Month","Product", ...). - Legend swatches carry
role="graphics-symbol"(the Graphics Module's own role for an atomic, repeated glyph) plus anaria-labelnaming the series. - Optional arrow-key stepping of the visual tooltip (
Chart'skeyboardprop, on by default) is an additive sighted-keyboard-user affordance layered on top of the hidden table, cited to Recharts'accessibilityLayeras a tier-3 opinion β never the only way to reach the data.
Keyboard
When keyboard: true (the default), Chart's own wrapping element (not the SVG itself) is
focusable (tabindex="0", role="group", aria-roledescription="chart"):
ArrowRight/ArrowLeftstep the active data point (clamped, no wraparound); swapped under an RTL context.Home/Endjump to the first / last data point.Escapeclears the active point and closes the tooltip.
Hovering a data point's invisible hit-band does the same thing via pointer input β both paths
drive the same active_index state, so the tooltip and the visual cursor line always agree with
whichever input method is in use.
Dense x-axes
A chart with many data points (e.g. 90 daily values) would draw one x-axis tick label per datum by
default and overlap them into an unreadable smear. Chart's max_x_ticks prop (default 12)
caps how many tick labels are drawn β it labels only every ceil(n / max_x_ticks)-th datum
(always including the first), leaving every hit band, mark, and hidden-table row exactly as before;
this thins the visible axis labels only, never the underlying data or interactivity. This is MVP
count-based thinning, not width-aware β a chart with unusually long labels at a narrow viewport can
still overlap even within this limit; deriving the count from estimated rendered label width
instead of a fixed count is the natural follow-up (see dev-docs/research/chart-forks-2026-09-19.md).
Variants
Alternative examples for common configurations.
bar
| Category | Desktop | Mobile | Tablet |
|---|---|---|---|
| January | 180 | 124 | 62 |
| February | 250 | 158 | 60 |
| March | 293 | 147 | 33 |
| April | 294 | 105 | 25 |
| May | 258 | 70 | 52 |
| June | 208 | 74 | 79 |
- Desktop
- Mobile
- Tablet
line
| Category | Desktop |
|---|---|
| January | 243 |
| February | 309 |
| March | 312 |
| April | 255 |
| May | 175 |
| June | 125 |
stacked
Stacked area
| Category | Desktop | Mobile |
|---|---|---|
| January | 154 | 115 |
| February | 199 | 126 |
| March | 227 | 92 |
| April | 231 | 56 |
| May | 212 | 63 |
| June | 181 | 110 |
- Desktop
- Mobile
Stacked bar
| Category | Desktop | Mobile |
|---|---|---|
| January | 154 | 115 |
| February | 199 | 126 |
| March | 227 | 92 |
| April | 231 | 56 |
| May | 212 | 63 |
| June | 181 | 110 |
- Desktop
- Mobile