Mantle Chart Root Mode Migration Guide
@ngrok/mantle's chart Roots (LineChart.Root, BarChart.Root, AreaChart.Root, and ScatterPlot.Root) take a mode prop in place of the decorative boolean. mode has three values: "interactive" (the default, the chart as it always was), "static" (new: a named image with a description, no Tab stop, no hover, no tooltip, no table), and "decorative" (the old decorative behavior). This guide moves a codebase from the boolean to the prop and names the charts that belong in the new mode.
TL;DR: Replace decorative with mode="decorative" on every chart Root. Then set mode="static" on every chart a reader glances at while its values sit beside it as text, such as a sparkline in a table row: drop its Tooltip part, keep its name, and leave animate off.
Codemod
From a mantle checkout:
In a project that installs @ngrok/mantle:
The rules apply step 2. On the four chart Roots, decorative becomes mode="decorative", and decorative={flag} becomes mode={flag ? "decorative" : "interactive"}. Steps 3 to 5 stay manual, because whether a chart is a sparkline is a design call that no rule can read off the JSX. The rules read a part through its import from @ngrok/mantle/<chart>, under its own name or an alias. A part that reaches the file through a local re-export or a destructured namespace stays manual. A function that binds a chart's name to a parameter or a local shadows the import, so the rules leave that function alone. Pass --dry-run to print the rewrites first, and run your formatter after the rewrite.
You are an agent migrating a codebase. Do this:
- Find every chart Root with the
decorativeprop:LineChart.Root,BarChart.Root,AreaChart.Root, andScatterPlot.Rootfrom@ngrok/mantle. Search fordecorativenext to one of those names, and for a renamed import of the same part. - For each, replace
decorativewithmode="decorative". When the value is dynamic (decorative={isPlaceholder}), writemode={isPlaceholder ? "decorative" : "interactive"}. Nothing else changes: a decorative chart still forbidsaria-label,aria-labelledby,activeIndex,onActiveIndexChange, andonDatumActivate. - Find every chart Root that is a sparkline, a trend in a card, or a chart in a print view: a Root with a
classNamethat sets a small fixed size (h-6 w-28), a Root inside aDataTable.Cell, or a Root with no axes and one series beside a number. For each, addmode="static"and delete itsTooltippart (<BarChart.Tooltip />and the like); a static chart shows no readout. Keep thearia-labeloraria-labelledby: a static chart requires a name. DeleteactiveIndex,onActiveIndexChange, andonDatumActivatefrom it: a static chart forbids them. - Leave
animateoff a static chart unless the product wants the reveal.animatedefaults tofalsein static mode andtruein the other two. - Find a test that reaches a sparkline through
getByRole("application", …)oruser.tab(). A static chart isgetByRole("img", { name }), and it is not a Tab stop. - Leave every other chart Root alone. The default mode is unchanged.
- After editing, run the project's formatter, linter, type checker, and tests.
Why a mode? Two booleans could not say "noninteractive but not decorative". A sparkline beside a total still tells a reader when activity happened and how it varied, so it keeps a name and a description. It has no Tab stop, because one hundred sparklines in a table as one hundred Tab stops is a focus order no reader wants, and no hover readout, because a value only a pointer can reach fails WCAG 1.4.13.
modenames the three contracts, and the type forbids the props each one has no use for.
Before → after
A placeholder behind an empty state
A sparkline in a table cell
A test that reaches a sparkline
What it buys on a list page
The chart benchmark page in the docs site (/bench/charts) renders 100 rows with two bar sparklines each in a DataTable, 30 bars per chart, with the mount animation off. The same production build of that page ran against the previous mantle and against this one. Chromium 153 headless, DPR 2, CPU 1×, medians of 3 runs.
The charts' share of the mount went from about 1,260 ms to about 35 ms, 0.17 ms per sparkline. Most of the gain is the dormant mount, which every chart gets on upgrade. mode="static" adds the rest: no overlay, no keyboard sentence, and no tooltip registration per cell, 700 fewer DOM nodes, and a hover sweep that costs 40% less, because no cell runs hover handling.
What each mode renders
The description (data-slot="<kind>-description") renders in interactive and static mode. In static mode it ends after the series labels; the keyboard instructions belong to the overlay a static chart does not have.