Loading states

Hold isLoading true while the data is in flight. The chart shows the shape it is about to be, at the height it will occupy, and cross-fades into the plot when the flag drops — same grid cell, same size, so nothing on the page moves when the marks land.

tsx
<Chart.Line
  categories={categories}
  height={320}
  isLoading={revenue.isPending}
  series={series}
/>

The placeholder is derived from the props the chart already has: one legend row per series, and axis rows only where an axis is visible. There is nothing to configure and nothing to keep in sync when the chart changes.

The first frame is a loading state too. A chart has to read its colors off the document before it can paint, so the built-in placeholder also covers that frame — with isLoading false and data in hand. The wrapper carries aria-busy while either is true, and the placeholder itself is aria-hidden.
Shape-matched placeholders are a web feature. isLoading means the same thing on iOS and Android, but there it draws the platform's own spinner: no .Skeleton component and no skeleton slot, because there is no DOM to build one out of.

Skeleton props

Every form also exposes its placeholder on its own, as Chart.Line.Skeleton — for when the chart is not mounted yet at all: a Suspense fallback, a route placeholder, a dashboard slot whose query has not started. These props apply only there; a chart driven by isLoading fills them in itself.

tsx
<Suspense fallback={<Chart.Line.Skeleton height={320} legendCount={2} />}>
  <Revenue />
</Suspense>
PropTypeDefaultDescription
heightnumber240Reserved height. Match the chart it stands in for.
legendCountnumber0Legend rows to reserve. Drawn from two up — a single series gets no legend, so reserving a row for one would leave a gap the chart never fills.
xAxisbooleantrueReserves the horizontal-axis label row.
yAxisbooleantrueReserves the vertical-axis label column.
classNamestringCSS class applied to the skeleton root.

Custom skeleton

skeleton takes a rendered element, not a component, and replaces the built-in one while isLoading is true. Keep its height equal to the chart's so the swap still costs no layout shift.

tsx
function RevenueSkeleton({ height = 320 }) {
  return (
    <div
      aria-label="Loading revenue chart"
      aria-busy="true"
      role="status"
      style={{ height }}
    >
      <div className="skeleton-title" />
      <div className="skeleton-plot" />
    </div>
  )
}

<Chart.Line
  isLoading
  skeleton={<RevenueSkeleton height={320} />}
  height={320}
  axis={{ x: false, y: true }}
  categories={categories}
  series={series}
/>
It covers isLoading only. The frame before the first paint still uses the built-in placeholder, because that one is derived from the chart and always matches it. Legend rows and axis gutters are yours to mirror here — axis shapes the built-in placeholder, not this one.