Component

chart

dx components add chart

A themed, data-driven SVG chart (area, bar, line) with tooltip, legend, and an accessible hidden data table.

Visitors
Total visitors for the selected range
Visitors by day, desktop and mobileAprAprAprAprMayMayMayMayJunJunJunJun
Visitors by day, desktop and mobile
DateDesktopMobile
Apr 2236190
Apr 3250191
Apr 4264189
Apr 5277186
Apr 6289181
Apr 7301174
Apr 8312165
Apr 9322156
Apr 10330145
Apr 11338134
Apr 12344122
Apr 13349111
Apr 14353100
Apr 1535590
Apr 1635580
Apr 1735473
Apr 1835266
Apr 1934862
Apr 2034359
Apr 2133758
Apr 2233059
Apr 2332163
Apr 2431268
Apr 2530175
Apr 2629083
Apr 2727993
Apr 28267104
Apr 29255116
Apr 30242128
May 1230140
May 2218152
May 3206164
May 4195174
May 5185184
May 6175192
May 7167198
May 8159202
May 9153205
May 10148205
May 11144204
May 12141200
May 13140195
May 14141188
May 15143180
May 16146170
May 17151159
May 18157148
May 19164137
May 20173125
May 21182114
May 22193104
May 2320595
May 2421787
May 2523081
May 2624476
May 2725873
May 2827273
May 2928674
May 3030077
May 3131383
Jun 132790
Jun 233998
Jun 3351108
Jun 4362119
Jun 5372131
Jun 6381143
Jun 7389155
Jun 8395167
Jun 9400179
Jun 10404189
Jun 11406199
Jun 12407207
Jun 13406213
Jun 14404217
Jun 15400220
Jun 16395220
Jun 17389219
Jun 18382215
Jun 19374210
Jun 20364203
Jun 21354194
Jun 22343184
Jun 23332174
Jun 24320163
Jun 25307151
Jun 26295140
Jun 27283129
Jun 28271118
Jun 29259109
Jun 30248102
  • 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-empty aria-label (Chart's aria_label prop), 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">) carries Chart's x_label prop (default "Category") rather than being left empty β€” an empty <th> has no accessible name and fails axe's empty-table-header rule, 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 an aria-label naming the series.
  • Optional arrow-key stepping of the visual tooltip (Chart's keyboard prop, on by default) is an additive sighted-keyboard-user affordance layered on top of the hidden table, cited to Recharts' accessibilityLayer as 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 / ArrowLeft step the active data point (clamped, no wraparound); swapped under an RTL context.
  • Home / End jump to the first / last data point.
  • Escape clears 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

Visitors by month, desktop, mobile, and tabletJanFebMarAprMayJun
Visitors by month, desktop, mobile, and tablet
CategoryDesktopMobileTablet
January18012462
February25015860
March29314733
April29410525
May2587052
June2087479
  • Desktop
  • Mobile
  • Tablet

line

Visitors by month, desktopJanFebMarAprMayJun
Visitors by month, desktop
CategoryDesktop
January243
February309
March312
April255
May175
June125

stacked

Stacked area

Visitors by month, desktop and mobile, stackedJanFebMarAprMayJun
Visitors by month, desktop and mobile, stacked
CategoryDesktopMobile
January154115
February199126
March22792
April23156
May21263
June181110
  • Desktop
  • Mobile

Stacked bar

Visitors by month, desktop and mobile, stackedJanFebMarAprMayJun
Visitors by month, desktop and mobile, stacked
CategoryDesktopMobile
January154115
February199126
March22792
April23156
May21263
June181110
  • Desktop
  • Mobile