TanStack
API Reference

Focus and Interaction

The DOM hosts, framework adapters, and React Native Chart provide point-level interaction from each mark's emitted ChartPoint values and rendered scene primitives. The defaults cover geometry-aware pointer or responder focus, linear keyboard or accessibility navigation, activation, and an optional native tooltip. Definitions own these policies; adapters mount them and report events.

Default behavior

With no custom focus strategy:

  • pointer movement finds the nearest point within maxFocusDistance
  • pointer leave or cancellation clears unpinned focus
  • the SVG uses tabIndex (0 by default) when keyboard is enabled
  • focusing the SVG selects the first point in the keyboard task order
  • arrow keys move through points sorted by pixel x, then pixel y
  • Home and End move to the first and last point
  • Enter and Space toggle an enabled sticky tooltip and call onSelect for the focused point
  • a click focuses and selects the nearest point, or selects null
  • the renderer's focus ring follows the primary point

maxFocusDistance defaults to 48 scene pixels. Set tabIndex to control normal tab-order participation while keeping keyboard handling enabled. Set keyboard: false to remove keyboard navigation and force tab index -1. Authored whenFocused marks compose with the primary-point ring. Set definition focusRing: false only when authored focus geometry replaces that indicator.

Set pointer: false when the application owns the pointer gesture but still wants chart focus, focus marks, and tooltips. Automatic pointer move, leave, and click handling stops; keyboard navigation remains independent. Resolve and paint application-owned focus through host.interaction or the interaction controller reported by onRender.

Interaction geometry

When neither focus nor spatialIndex is supplied, pointer resolution has two stages:

  1. The topmost interactive scene primitive containing the pointer wins. The resolver traverses the final scene in reverse paint order and applies nested translations and clips.
  2. If no primitive contains the pointer, its interaction.affinity ranks the fallback. x and y compare distance from that axis boundary first and use complete geometry distance to break ties; xy compares complete geometry distance; geometry has no off-shape fallback.

maxFocusDistance applies to that primary boundary distance, not necessarily to the point used as the tooltip and keyboard anchor. A semantic point that is not attached to a scene primitive retains anchor-based two-dimensional distance.

The primitive is the geometry source of truth: rect includes its rounded corners, dot uses its radius, area uses its polygon, and polyline and rule use their stroked paths. Its interaction attaches either one semantic point or the ordered points represented by a continuous primitive. Groups and labels cannot carry interaction metadata.

Built-in marks attach these natural defaults:

MarkScene primitiveFallback
barYRounded rectanglex
barXRounded rectangley
lineYStroked polylinex
areaYFilled areax
areaXFilled areay
rect, dot, hexagonRect, circle, polygonxy
bandXRounded rectanglex
bandYRounded rectangley

Facet layout rewrites the primitive's attached point references while leaving the primitive in local coordinates. The resolver therefore observes the same post-layout translations and clips as SVG and Canvas instead of maintaining a second geometry copy. Inline mark states similarly return a resolved scene to the host; during a transition, pointer selection intentionally follows that destination scene rather than interpolating a second hit-test scene.

With an active axis viewport, the host calls viewportInteractionPoints(scene, presentationPoints) before pointer, keyboard, custom-focus, or spatial-index resolution. Off-window anchors from clipped viewport content remain in scene.points but are not focus candidates; fixed-ownership mark points remain eligible. Direct scene consumers can pass that filtered list as the optional final argument to findNearestPoint(scene, x, y, maxDistance, points) so primitive hit testing uses the same candidate set.

For curved polyline and area nodes, the current resolver uses the primitive's structured point geometry. Exact picking against an optional authored SVG path string remains a separate refinement.

Built-in axis focus modes compose painted containment with axis snapping. When the pointer is inside an interactive primitive, the topmost painted mark seeds the primary point. Outside painted geometry, the configured axis mode uses its normal nearest-axis fallback. A custom strategy replaces this host behavior.

Focus modes

Use a preset for built-in focus behavior:

ts
import { tooltip } from '@tanstack/charts/tooltip'

const groupedDownloads = defineChart(definition, {
  focus: 'group-x',
  tooltip,
})
PresetPointer resolutionGroup returned to callbacks and tooltipKeyboard navigation
nearestNearest painted geometry or point in two dimensionsPrimary point onlyEvery point
nearest-xContaining painted mark; otherwise nearest x, then nearest yPrimary point onlyEvery point
nearest-yContaining painted mark; otherwise nearest y, then nearest xPrimary point onlyEvery point
group-xContaining painted mark; otherwise nearest x, then nearest y at that xOne point per group sharing the semantic x value; primary point firstOne representative per semantic x value
group-yContaining painted mark; otherwise nearest y, then nearest x at that yOne point per group sharing the semantic y value; primary point firstOne representative per semantic y value

Grouping compares semantic values, including dates by timestamp. Duplicate points with the same group value are reduced to one member in grouped focus.

The equivalent focusX, focusY, focusNearestX, and focusNearestY strategy objects remain available from @tanstack/charts/focus for composition or direct strategy use. The exact exported objects receive the same host-level containment behavior as their presets. A strategy that wraps or copies one of them is custom and owns its complete pointer resolution.

Crosshair guides

crosshair is a data-less presentation mark. It follows the chart's resolved focus state without adding scale values or pointer targets:

ts
import { crosshair, defineChart, lineY } from '@tanstack/charts'

const definition = defineChart({
  marks: [
    lineY(rows, { x: 'date', y: 'value', color: 'series' }),
    crosshair({
      x: { label: true },
      y: false,
      strokeDasharray: '4 4',
    }),
  ],
  x: { scale: scaleUtc },
  y: { scale: scaleLinear },
  focus: 'group-x',
  maxFocusDistance: Number.POSITIVE_INFINITY,
})

The x guide is vertical and the y guide is horizontal. Both default to enabled; labels and the intersection marker default to disabled. Shared rule options apply to both axes, and an axis object can override them. A label uses the matching resolved tick label when available, falls back to the semantic value, and accepts a format callback. Labels are clamped to the chart surface and rendered with a configurable halo. Rules and the optional marker are clipped to the plot. For difference intervals such as stacked bars and areas, the label formats the plotted x2 or y2 endpoint so it matches the guide; tooltip formatting still reports the interval difference.

crosshair<TXValue, TYValue>(options = {}) accepts these top-level options:

OptionDefaultMeaning
idcrosshair-${markIndex}Stable mark and guide identity
xtrueVertical rule or categorical band
ytrueHorizontal rule or categorical band
markerfalseMarker at the resolved x/y intersection
motionNo mark-specific overrideOptional guide spring or tween configuration

The shared rule options apply to both axes. The same fields on an axis object override the shared value for that axis:

Rule optionDefaultMeaning
strokeChart foregroundRule color
strokeOpacity0.35Rule stroke opacity
strokeWidth1Nonnegative rule width
strokeDasharrayNoneSVG-compatible stroke dash pattern

An x or y axis object adds two options:

Axis optionDefaultMeaning
labelfalsetrue for defaults or a label options object
bandfalsetrue for defaults or a band options object

On a categorical axis, band replaces that axis rule with a plot-spanning cursor band centered on the focused value:

ts
marks: [
  crosshair({
    x: {
      band: {
        inset: 0,
        radius: 3,
        fill: '#64748b',
        fillOpacity: 0.16,
      },
      label: true,
    },
    y: false,
  }),
  barY(rows, {
    x: 'period',
    y: 'value',
    color: 'series',
    inset: 4,
  }),
  crosshair({
    x: false,
    y: { strokeDasharray: '4 4', label: true },
  }),
]

The first guide is an underlay because it precedes the bars; the second is an overlay. With a bar inset of 4 and band inset of 0, the cursor extends exactly 4 pixels beyond each bar edge. Its x label shows the focused period; the y rule label shows the focused stack endpoint. Use separate crosshair marks whenever axes need different placement.

Band optionDefaultMeaning
inset0Pixels removed from both scale-band edges; negative values create outset
radiusNoneCorner radius
fillChart foregroundFill color
fillOpacity0.12Fill opacity
strokeNoneOptional outline color
strokeOpacityNoneOptional outline opacity
strokeWidthNoneOptional nonnegative outline width
opacityNoneOpacity applied to the complete band

band: true uses those defaults. Band geometry uses the resolved scale bandwidth, spans the plot in the other direction, and remains clipped to the plot. A continuous scale or any other axis with zero bandwidth emits no band. The axis label can still be enabled because band replaces only the rule.

Label options control formatting and paint outside the plot:

Label optionDefault
formatMatching scale tick label, then semantic value
offset8
fillChart foreground
fillOpacityNone
strokevar(--ts-chart-crosshair-label-halo, Canvas)
strokeOpacityNone
strokeWidth3
opacityNone
fontSize11
fontWeightNone

Marker options control the resolved x/y intersection marker:

Marker optionDefault
radius4
fillvar(--ts-chart-crosshair-marker-fill, Canvas)
fillOpacityNone
strokeFocused point color, then guide or theme color
strokeOpacityNone
strokeWidth2
opacityNone

Pass semantic axis types to crosshair when TypeScript should check label formatters independently from the surrounding definition:

ts
crosshair<Date, number>({
  x: { label: { format: (value) => value.toISOString() } },
  y: { label: { format: (value) => value.toFixed(1) } },
})

Mark order controls placement. Put crosshair(...) before the first ordinary mark for an underlay, or after it for an overlay. The default primary focus ring still composes with the crosshair; set focusRing: false only when the crosshair marker or other authored geometry deliberately replaces it.

The guide is aria-hidden. Pointer and keyboard users reach the same focus state through the chart root, callbacks, and optional tooltip. motion on the crosshair controls its keyed rules, bands, labels, and marker when the definition is mounted with the optional motion renderer. Active guide placements remain visible through keyed scene updates so restored focus animates from the previous geometry.

Controlled cursors

Import the controller and host extension from the isolated cursor entry when interaction state must be shared or set programmatically:

ts
import { createChartCursor, cursorHost } from '@tanstack/charts/cursor'

const sharedDate = createChartCursor<Date, number>()

const definition = defineChart({
  marks: [
    lineY(rows, { x: 'date', y: 'value' }),
    crosshair({ x: { label: true }, y: false }),
  ],
  x: { scale: scaleUtc },
  y: { scale: scaleLinear },
  focus: 'group-x',
  cursor: {
    use: cursorHost,
    controller: sharedDate,
    mode: 'focus',
    match: 'x',
    pin: true,
  },
})
ts
function createChartCursor<
  TXValue extends ChartValue = ChartValue,
  TYValue extends ChartValue = ChartValue,
>(
  initialState: ChartCursorState<TXValue, TYValue> | null = null,
): ChartCursorController<TXValue, TYValue>

Every definition cursor binding accepts these common fields:

OptionDefaultMeaning
useRequiredCursor host extension, normally cursorHost
controllerRequiredObservable structural cursor controller
modeRequiredfocus or free binding discriminator
pinfalseEnables activation to pin and dismiss

Mode-specific fields are:

ModeOptionDefaultMeaning
focusmatchxySemantic axes shared between hosts
freexNoneOptional x-axis valueAt semantic mapping
freeyNoneOptional y-axis valueAt semantic mapping

Each free-axis object accepts an optional valueAt(context) callback. Without it, the cursor still shares scene and normalized coordinates but has no semantic value for that axis.

createChartCursor remains a three-method structural store. cursorHost opts the binding into platform-neutral cursor policy without adding that policy to charts that do not use cursors. Adapter and renderer authors can import the projection, focus, presentation, and session helpers from @tanstack/charts/cursor/host; application code normally uses the token from @tanstack/charts/cursor.

ChartCursorExtensionToken is the environment-neutral binding contract. cursorHost implements it as a ChartCursorHostExtension. Adapter authors call createChartCursorHostSession(binding) to create an ownership-safe ChartCursorHostSession for one mounted binding.

That host entry uses createFocusChartCursorState and createFreeChartCursorState to publish state, then resolveChartCursorPresentation and resolveChartCursorFocus to project it into each scene. resolveChartPointerFocus composes built-in axis focus with painted containment and returns undefined for default nearest focus so a host can apply its spatial index or renderer geometry. resolveChartFocusStrategy converts a built-in focus name to its grouping strategy. resolveFocusPresentation finally produces renderer-neutral underlay and overlay nodes. An empty array from resolveChartPointerFocus means an explicit strategy found no target. Pass scene.points (the default) as its final argument to enable painted containment. A distinct array explicitly signals active renderer presentation points and preserves anchor resolution while those interpolated points are authoritative.

Use the same controller in several browser or React Native definitions to synchronize by semantic value rather than pixel position. In focus mode, pointer, responder, keyboard, or accessibility focus publishes a value-anchored cursor. Each subscribing host maps the value through its own scales, resolves its own local point and focus group, and paints its crosshair and tooltip. match defaults to xy; choose x or y for an axis cursor. The originating point's group is retained as the preferred series when more than one local point matches. Optional ChartCursorState.origin carries { key, markId, datumIndex } to retain the local point when equal values repeat, including across facets. A unique key plus markId survives data reordering; datumIndex disambiguates duplicate keys. A consuming scene ignores the tie-breaker when its key and mark do not exist, then resolves from the portable semantic value and preferred group.

free mode follows plot coordinates without resolving a datum:

ts
const freeCursor = createChartCursor<Date, number>()

const definition = defineChart(baseDefinition, {
  cursor: {
    use: cursorHost,
    controller: freeCursor,
    mode: 'free',
    pin: true,
    x: {
      valueAt: ({ scene, position }) =>
        xScale
          .copy()
          .range([scene.chart.x, scene.chart.x + scene.chart.width])
          .invert(position),
    },
    y: {
      valueAt: ({ scene, position }) =>
        yScale
          .copy()
          .range([scene.chart.y + scene.chart.height, scene.chart.y])
          .invert(position),
    },
  },
})

The host publishes normalized plot coordinates so the free cursor survives a responsive relayout. valueAt optionally adds semantic values for labels and application state; inversion and precision remain owned by the configured scale. Programmatic state may instead use anchor: 'value', anchor: 'normalized', or anchor: 'scene'. Only the anchor's coordinate space is authoritative; the other fields are diagnostics from the host that last emitted the state.

createChartCursor returns getState, setState, and subscribe. Passing null clears the cursor, and setState accepts a previous-state updater. Unpinned host-owned state clears on pointer or responder cancellation, leave, and blur. Cleanup is ownership-safe: a host clears only the exact unpinned state object it most recently published to that controller. State replaced by the application or another host, and pinned state, survives that host's cleanup, rebinding, and unmount.

With pin: true, click or tap pins. Browser focus cursors toggle through Enter or Space and dismiss through Escape; React Native focus cursors expose the equivalent activate and escape accessibility actions. A pinned free cursor can be toggled by another click or tap, or cleared programmatically.

Free mode deliberately does not invent keyboard datum navigation. When its values are part of the reader's task, pair it with a labeled semantic control or status output. A focus-mode cursor inherits the chart's existing keyboard navigation and accessible tooltip behavior.

Definition cursor bindings have browser and React Native host parity. DOM renderer hosts publish pointer and keyboard interaction. React Native Chart publishes responder gestures and, for focus mode, accessibility navigation, activation, and dismissal. Both hosts subscribe to programmatic updates and shared controllers. Free mode deliberately remains a coordinate gesture with no invented keyboard or accessibility datum order.

Disabling chart-owned focus

Set focus: false to disable native pointer and keyboard point focus. The scene keeps its semantic points but omits the generated default focus layer, and the DOM host forces the chart surface out of the tab order. Explicit focus-only marks remain available to custom renderers and programmatic paint.

ts
import { focusDisabled } from '@tanstack/charts/focus/disabled'

focusDisabled resolves, groups, and navigates to no points. Use it when an application owns gestures, selection paint, accessibility, and task semantics outside the native resolver but still needs the rendered focus layer. Set definition keyboard: false and omit its tooltip as appropriate for that application-owned interaction.

Custom focus strategies

ts
interface ChartFocusStrategy<
  TDatum = unknown,
  TXValue extends ChartValue = ChartValue,
  TYValue extends ChartValue = ChartValue,
> {
  resolve(
    points: readonly ChartPoint<TDatum, TXValue, TYValue>[],
    context: ChartFocusResolveContext,
  ): readonly ChartPoint<TDatum, TXValue, TYValue>[]

  group(
    points: readonly ChartPoint<TDatum, TXValue, TYValue>[],
    context: ChartFocusGroupContext<TDatum, TXValue, TYValue>,
  ): readonly ChartPoint<TDatum, TXValue, TYValue>[]

  navigation(
    points: readonly ChartPoint<TDatum, TXValue, TYValue>[],
  ): readonly ChartPoint<TDatum, TXValue, TYValue>[]
}

ChartFocusResolveContext contains scene-pixel x, y, and maxDistance. resolve returns the primary point first. ChartFocusGroupContext contains the point restored or reached through keyboard navigation. navigation returns the ordered keyboard task set.

ChartFocusMode accepts a ChartFocusPreset string or a ChartFocusStrategy.

Tooltips

Import tooltip from @tanstack/charts/tooltip and place it on the definition for the native accessible tooltip. The default is a structured label-value table. Grouped focus adds a shared-axis heading and one color swatch and value row per series. Visible axis labels are reused. Numbers use browser locale formatting; dates use stable UTC ISO formatting.

ts
interface ChartTooltipOptions<
  TDatum = unknown,
  TXValue extends ChartValue = ChartValue,
  TYValue extends ChartValue = ChartValue,
> {
  className?: string
  portal?: ChartTooltipPortalInput
  items?: readonly ChartTooltipItem<TDatum, TXValue, TYValue>[]
  sort?: ChartTooltipSort<TDatum, TXValue, TYValue>
  anchor?: ChartTooltipAnchor<TDatum, TXValue, TYValue>
  placement?: 'auto' | ChartTooltipPlacement | readonly ChartTooltipPlacement[]
  offset?: number
  content?: (
    points: readonly ChartPoint<TDatum, TXValue, TYValue>[],
    context: ChartTooltipContentContext,
  ) => ChartTooltipContent
  format?: (
    point: ChartPoint<TDatum, TXValue, TYValue>,
    context: ChartTooltipContentContext,
  ) => string
  formatGroup?: (
    points: readonly ChartPoint<TDatum, TXValue, TYValue>[],
    context: ChartTooltipContentContext,
  ) => string
  sticky?: boolean
}
OptionDefaultMeaning
classNameNoneClass appended after ts-chart-tooltip
portalNoneOptional top-layer or fixed-position transport
itemsAutomatic x/yOrdered rows for a single focused point
sortvisualGrouped row order
anchorpointPreset, per-axis coordinates, or coordinate resolver
placementautoFixed or ordered fallback box placements
offset10Scene-pixel gap between anchor and box
contentAutomatic rowsReturns a safe title and structured rows
formatNoneReplaces content with primary-point text
formatGroupNoneReplaces content with focused-group text
stickytrueEnables activation-to-pin and text selection

Formatting precedence is content, formatGroup, format, then the default. The text formatters do not parse HTML, and newlines are preserved. className is appended to ts-chart-tooltip.

ChartTooltipContentContext.pinned is false during transient inspection and true after activation. content, format, formatGroup, and item text receive the same context, so either structured or plaintext content can reveal additional detail when pinned.

Ordered point items

items is an ordered single-point row list. Use x, y, and group shorthands, a configured channel, a scalar datum field, or derived text:

ts
const detailedDefinition = defineChart(definition, {
  tooltip: {
    use: tooltip,
    items: [
      {
        channel: 'y',
        label: 'Revenue',
        text: (point) => currency(point.yValue),
      },
      {
        field: 'volume',
        label: 'Volume',
        text: (point) => compact(point.datum.volume),
      },
      {
        id: 'change',
        label: 'Change',
        text: (point) =>
          point.datum.change == null ? null : percent(point.datum.change),
      },
      'x',
      'group',
    ],
  },
})

Array order is row order. A nullish datum field or nullish text result omits the row. Adding group to items renders it as a row instead of the automatic single-point title. In grouped focus, the shared-axis item supplies the heading label and text, the opposite-axis item formats values, and the group item formats series names. sort orders those generated series rows. Additional grouped structure belongs in content.

sort accepts visual, color-domain, focus, or a typed point comparator. Visual order follows the marks across the screen: top-to-bottom for an x-group and left-to-right for a y-group.

Anchor and placement

anchor controls the scene coordinate followed by the box:

  • point follows the primary focused point.
  • pointer follows the current pointer and falls back to the point for keyboard focus.
  • group-center uses the center of the focused points' bounding box.
  • { x, y } chooses each coordinate from point, pointer, semantic value, group center, or a plot edge.
  • A resolver receives the focused points plus { focus, pointer, plot, surface, scales } and returns scene coordinates. A nullish or non-finite result falls back to the primary point.

placement accepts auto, one placement, or an ordered fallback list. The placements are top, top-right, right, bottom-right, bottom, bottom-left, left, and top-left. A single placement is fixed and shifted inside the surface. A list uses the first placement that fits; if none fits, it uses the least-overflowing candidate and shifts it inside. auto uses top, bottom, right, then left.

ts
const groupedDefinition = defineChart(definition, {
  tooltip: {
    use: tooltip,
    anchor: { x: 'plot-center', y: 'plot-top' },
    placement: ['top', 'right', 'left', 'bottom'],
    offset: 12,
  },
})

Import portal from @tanstack/charts/tooltip/portal and assign it to the tooltip's portal option when an ancestor clips overflow or creates an incompatible stacking context. The host opens the tooltip as a manual Popover in the browser top layer where supported, while retaining its chart DOM ancestry. If Popover is unavailable or fails, it moves the tooltip directly under the chart's ownerDocument body with fixed high-stack positioning. Both paths map the scene anchor to viewport coordinates, reposition during scroll, resize, and content resize, and collide against the viewport instead of the chart box.

Clicking, Enter, or Space pins the tooltip. The next activation unpins it. Escape unpins and clears focus. Set sticky: false to disable pinning. A display-only tooltip has role="status" and aria-live="polite".

content supports display-only rows. Every framework adapter can compose native content around those rows while preserving the definition's ordering, anchor, placement, portal, and pinning behavior. A pinned custom body has non-modal dialog semantics.

Callbacks

ts
interface ChartInteractionCallbacks {
  onFocusChange?: (point: ChartPoint | null) => void
  onFocusGroupChange?: (points: readonly ChartPoint[]) => void
  onSelect?: (point: ChartPoint | null) => void
}

Definitions infer callback datum and semantic x/y value types without adapter generics. Focus callbacks run only when the primary focus key changes, except that a scene rebuild with an existing focused key reports the point with its new coordinates and datum.

onSelect reports mouse clicks and keyboard activation. A click with no point reports null; Enter and Space do nothing until a point is focused.

Spatial indexes

The default lookup scans the cached scene targets linearly. Dense charts can inject an index:

ts
type ChartSpatialIndexFactory<
  TDatum = unknown,
  TXValue extends ChartValue = ChartValue,
  TYValue extends ChartValue = ChartValue,
> = (
  points: readonly ChartPoint<TDatum, TXValue, TYValue>[],
  context: ChartSpatialIndexFactoryContext<TDatum, TXValue, TYValue>,
) => ChartSpatialIndex<TDatum, TXValue, TYValue>

The host rebuilds the index when the scene or definition changes. The index owns its search algorithm and must apply maxDistance. Point-only factories can ignore the second argument; geometry-aware indexes can traverse context.scene and use primitive bounds as their acceleration layer. Use the granular spatial primitive appropriate to the data; the boundary is described in Scales.

Supplying an index also replaces default primitive containment and affinity ranking; the host does not add a linear safety scan after an indexed query. An index that wants identical geometry semantics should index scene-primitive bounds and perform exact shape checks on its candidates.

A custom focus strategy takes precedence over spatialIndex for pointer resolution.

Application-owned gestures

Brushes, zooming, dragging, scrolling, and selections can listen on a wrapper or use onRender to attach application behavior. Keep semantic state outside the scene and update a dynamic definition by replacing its identity.

Use a definition cursor binding for snapped or free crosshairs. Keep other semantic state outside the scene and update a dynamic definition by replacing its identity.

For application-timed datum inspection, use the stable controller instead of reimplementing coordinate conversion and focus:

ts
const position = interaction.clientToScene(event.clientX, event.clientY)
const target = interaction.resolvePointer(event.clientX, event.clientY)
interaction.setControlledFocus(target)
interaction.setControlledFocus(null)

The resolution contains the scene position, primary point, and focus group. Passing that resolution to setControlledFocus infers source: 'pointer' and re-resolves its held position after scene and presentation updates. Passing a raw ChartPoint defaults to source: 'programmatic'; either source can be overridden explicitly. Controlled focus is not cleared by unrelated pointer-leave or focus-out events. Pass { pinned: true } to make an enabled sticky tooltip interactive.

Clean up application listeners before the next attachment or unmount. For a completely independent renderer or interaction layer, use the scene and extension contracts in Custom extensions.