TanStack
Guides

Responsive Charts

TanStack Charts treats width and height differently:

  • width is normally measured from the chart container;
  • height is a product decision supplied as pixels or an aspect ratio;
  • scale factories infer domains while configured instances retain fixed domains;
  • TanStack Charts copies those scales and assigns responsive pixel ranges.

This keeps a definition stable while the same chart moves between a dashboard card, a split pane, and a full-width report.

Container-responsive width

Omit width from mountChart or framework adapter options to follow the container. The shared DOM host observes the container and updates only when its measured width changes.

ts
const host = mountChart(element, {
  definition,
  height: 320,
  ariaLabel: 'Weekly downloads',
})

The container must have a resolvable width. In grid and flex layouts, the common requirement is min-width: 0 on the grid or flex child:

css
.chart-card {
  min-width: 0;
}

Supply width only when an application deliberately owns fixed geometry, such as an export frame or a benchmark.

Height and aspect ratio

Use one of these policies:

  • height: fixed product height in CSS pixels;
  • a positive, finite aspectRatio: derive height from the measured width;
  • neither: use the host default.

Do not supply both as competing policies. A fixed height is usually more stable for dashboards and scrolling pages. An aspect ratio is useful for editorial layouts where the chart should scale as one visual block. Invalid ratios fall back to the default height.

Automatic guide space

Omit margin for normal charts. The scene solver reserves the minimum space required by:

  • formatted tick labels;
  • rotated tick bounds;
  • first and last tick overhang;
  • axis titles;
  • legends;
  • inherited font metrics in a DOM host.
ts
const definition = defineChart({
  marks: [barX(rows, { x: 'value', y: 'label' })],
  x: {
    scale: scaleLinear,
    nice: true,
    label: 'Weekly downloads',
  },
  y: {
    scale: () => scaleBand<string>().padding(0.1),
  },
})

An explicit side locks only that side:

ts
margin: {
  left: 96
}

margin: 0 locks every side and is appropriate for guide-free sparklines. Automatic margins guarantee containment, not collision-free labels. Use ticks, tickRotate, or a different representation when labels compete for the same axis space.

Text measurement

Static scenes use deterministic text estimates. DOM and framework SVG hosts measure painted text with the inherited container font and relayout after web fonts finish loading.

Use measureText only when another renderer or layout system owns text measurement. It receives the text, font size, weight, anchor, and baseline and returns the painted box relative to the requested origin.

The resolved geometry is available after every render:

ts
const scene = host.getScene()

scene.margin
scene.chart
scene.scales

Use scene.chart to align application-owned overlays. Do not calculate a parallel plot rectangle from guessed margins.

Deterministic server output

A server cannot measure a future browser container. Supply initialWidth when the initial SVG must have deterministic geometry:

tsx
<Chart
  definition={definition}
  initialWidth={640}
  height={320}
  ariaLabel="Weekly downloads"
/>

The client adopts the real container width after hydration. Use the same initialWidth for all requests that render the same layout; do not derive it from browser-only APIs on the server.

See SSR and Hydration for the complete lifecycle.

Responsive construction

Most data transforms should depend only on application data. The responsive builder receives the full scene width and height, so it can choose breakpoints, tick counts, or a surface-relative representation.

Those values are not the final inner plot bounds. Automatic guides and legends resolve scene.chart after the builder returns. Exact plot-space collision, binning, or label placement belongs in a custom mark's render phase, which receives the final chart bounds and resolved scales, or in an application overlay driven by onRender.

ts
function summarizeRows(rows: Input['rows']) {
  return summarize(rows)
}

function createDefinition(rows: Input['rows']) {
  const summary = summarizeRows(rows)

  return defineChart(({ width }) =>
    buildResponsiveSpec(summary, {
      compact: width < 480,
    }),
  )
}

For a deliberately guide-free margin: 0 scene, the full surface and inner plot can coincide. Do not assume that equivalence for ordinary automatic layout.

Responsive relayout commits immediately even when animation is enabled for data updates. Set animate: { resize: true } only when size interpolation is intentional.

The Dynamic Data and Animation guide explains the two phases. The D3 integration contract explains why application code should not set positional pixel ranges.

Responsive checklist

  • The container can shrink because its grid or flex child uses min-width: 0.
  • Width is omitted unless fixed geometry is intentional.
  • Height or aspect ratio is an explicit product choice.
  • Scale domains remain semantic and independent from pixels.
  • Automatic margins are enabled unless the chart is deliberately guide-free.
  • Long labels are tested at the smallest supported width.
  • Overlays use scene.chart, not duplicated margin math.
  • SSR uses a deterministic initialWidth.
  • Final plot-space work reads custom-mark render bounds or scene.chart.