Research and implementation date: 2026-07-30
A React Native SVG host is viable. The chart grammar, D3 scales, marks,
responsive scene compiler, gradients, clips, polar and geographic paths, point
identity, and most focus algorithms can run unchanged. The current web
ChartRenderer cannot be the native abstraction: its public contract requires
DOM elements, string prerendering, client coordinates, and browser-owned
surface lifecycle.
The proof should advance only as a separate package and host:
definition + D3 scales + marks
│
▼
shared @tanstack/charts runtime
│
▼
ChartScene
│
▼
React Native interaction + react-native-svg
This can remain additive for current consumers. Do not generalize
ChartRenderer or remove DOM types from the root API to make native fit.
The POC establishes technical feasibility, package isolation, approximate JavaScript cost, and the main API boundaries. It does not establish device visual parity, application binary cost, Hermes runtime performance, or production support.
The publishable experimental @tanstack/react-native-charts package includes:
- a generic React Native
Chartcomponent that ownscreateChartRuntime; - responsive sizing from explicit dimensions, aspect ratio, or
onLayout; - a direct keyed
ChartScenevisitor usingreact-native-svg; - groups, rules, polylines, areas, dots, rectangles, labels, linear gradients, nested translation, and rectangular clips;
- current-color and CSS-variable-fallback paint resolution;
- nearest, axis-nearest, and grouped focus;
- tap selection, sticky pinning, focus restoration, and accessibility actions;
- a separate SVG focus overlay;
- inactive shared focus-layer groups are omitted from the base scene;
- an optional native tooltip subpath with point, pointer, group-center, and custom anchors, collision-aware placement, custom content, and a custom React body;
- an optional injected text measurer;
- a bare Metro fixture using React Native 0.86.2 and
react-native-svg15.15.5; - an Expo 57 Metro fixture using Expo's
react-native-svg15.15.4; - source-map checks that reject browser renderer code in iOS and Android bundles.
The host imports runtime values from narrow core subpaths. It does not import
the root barrel, react-dom, a browser adapter, the DOM renderer, the Canvas
host, or the SVG string/reconciliation pipeline. The full-chart Metro fixture
imports its application definition from @tanstack/charts/universal; the core
line control keeps granular imports for a tighter comparison.
That distinction matters to shared application source. A Metro probe using
defineChart and lineY from the browser-oriented root
@tanstack/charts barrel traversed the DOM renderer, DOM text, reconciliation,
SVG surface, and adapters. The prerequisite universal-barrel PR adds
@tanstack/charts/universal for normal environment-neutral authoring while
retaining granular entries for tighter bundles.
Metro 0.84 retains every runtime module re-exported by /universal, including
unused marks and the environment-neutral static SVG string serializer. It does
not retain the guarded DOM or browser host modules. The broad entry is the
simplest supported authoring path; bundle-sensitive native applications can
use granular mark, runtime, and scene entries until Metro can eliminate those
unused re-exports.
That prerequisite also adds @tanstack/charts/types, exports the generic
tooltip and portal token interfaces for host-adapter authors, and moves DOM
host types to an internal dom-types.ts module while preserving their root
re-exports. This POC adds no further core public API.
Minimum usage keeps the browser-oriented root unchanged and selects the native host and tooltip explicitly:
import { scaleLinear } from 'd3-scale'
import { defineChart, lineY } from '@tanstack/charts/universal'
import { Chart } from '@tanstack/react-native-charts'
import { tooltip } from '@tanstack/react-native-charts/tooltip'
const definition = defineChart({
marks: [lineY([4, 9, 7])],
x: { scale: scaleLinear().domain([0, 2]) },
y: { scale: scaleLinear().domain([0, 10]) },
tooltip: { use: tooltip, sticky: true },
})
export function RevenueChart() {
return (
<Chart
definition={definition}
accessibilityLabel="Revenue"
aspectRatio={1.5}
/>
)
}The tooltip token makes that configured definition native-specific. A shared application can keep the chart spec and tooltip options universal, then attach the web or native token in its platform entry.
Production, minified ESM; React, React Native, and react-native-svg are
external in the native entries.
| Entry | Minified | Gzip |
|---|---|---|
| React Native SVG host | 26.95 kB | 10.22 kB |
| React Native SVG host with tooltip | 32.71 kB | 12.17 kB |
| React Native line consumer | 49.18 kB | 19.03 kB |
| Shared line scene | 38.17 kB | 14.82 kB |
| Existing React web adapter | 39.98 kB | 14.57 kB |
| Existing React web line consumer | 62.03 kB | 23.41 kB |
The native host entry includes the host plus every shared runtime and D3 module
reachable from exporting Chart; it is not isolated adapter-only overhead.
The base host and line consumer omit Tooltip.tsx. Importing the tooltip
subpath adds 5.76 kB minified and 1.94 kB gzip. The native line consumer
additionally includes the line mark and D3 scale code while leaving platform
peers external. It is the closest comparison to the existing React line entry,
but the two platforms do not have identical runtime baselines. The native
entries are informational measurements, not locked budgets.
React Native production bundles were minified for both platforms. Each row is
measured against a blank React Native application built with the same Metro
configuration. The full chart uses /universal; the core line control uses
granular entries.
| Platform | Full /universal chart minified JS delta |
react-native-svg gzip delta |
Core line gzip delta | Full /universal chart gzip delta |
Full chart module delta |
|---|---|---|---|---|---|
| iOS | 434.49 KiB | 27.75 KiB | 38.94 KiB | 103.00 KiB | 384 |
| Android | 434.64 KiB | 27.69 KiB | 38.94 KiB | 103.05 KiB | 384 |
The full iOS bundle was 1,328,999 bytes and 319,853 bytes gzip, versus 884,080
and 214,383 for blank. Android was 1,334,515 and 320,921, versus 889,448 and
215,397 for blank. Compared with the otherwise identical granular fixture,
/universal retains another 119.06 KiB minified and 28.91 KiB gzip on both iOS
and Android, plus 102 modules per platform under this Metro version.
These are JavaScript bundle measurements, not Hermes bytecode or installed
application sizes. They exclude the native iOS and Android code linked by
react-native-svg. A clean release-binary before/after comparison is still
required.
The full-chart source maps require /universal and include its static SVG
serializer. They exclude every guarded browser module: adapters, DOM host, DOM
text measurement, DOM renderer, Canvas, reconciliation, SVG resources/surface,
react-dom, React web charts, and Octane charts.
| Capability | POC result | Production boundary |
|---|---|---|
| Definitions, D3 scales, marks, layout | Shared unchanged | High confidence |
| Cartesian, facet, polar, geo geometry | Shared scene paths | Needs device conformance |
| All seven scene node kinds | Implemented | Needs iOS/Android visual tests |
| Linear gradients and rectangular clips | Implemented | Device and nested-clip tests remain |
| Responsive sizing | onLayout, explicit size, aspect ratio |
Scroll/layout integration remains |
| Paint | currentColor and CSS fallback resolution |
Dynamic definitions still see the web default theme |
| Text | SVG text plus injected core measurer | Exact native typography and async measurement are unresolved |
| Nearest and grouped focus | Implemented | Logic is duplicated from the DOM renderer |
| Focus presentation | Generic native overlay | Authored focus marks and mark states are not applied |
| Tap, selection, sticky tooltip | Implemented | Responder ownership may conflict with scrolling and gestures |
| Tooltip | Native in-tree overlay | Web className, portal semantics, and full item formatting are not equivalent |
| Accessibility | Labeled adjustable root and actions | No per-point native accessibility tree or data-table escape |
| Motion | Not implemented | Requires a renderer-neutral transition model |
| SVG/PNG export | Not implemented | Needs native snapshot/share APIs |
| Large-data renderer | Not measured | SVG node pressure may require an optional Skia host |
| SSR and hydration | Intentionally absent | Web-only behavior |
- SVG
arcsandmiter-clipjoins are explicitly normalized toroundandmiter, respectively, because the React Native SVG type surface does not expose them. - The native paint resolver can consume CSS variable fallbacks, but it cannot evaluate application CSS. A production host needs concrete native theme values before scene compilation.
- The current text-measure contract does not carry font family, font style,
letter spacing, direction, or locale. Passing
fontFamilyto the painter cannot make guide layout measure the same font. react-native-svgsupports the required primitive family and Fabric, but SVG compatibility must be verified on real iOS and Android renderers rather than inferred from server-rendered component markup.
- Focus preset resolution, point restoration, navigation order, tooltip content, anchor, and placement remain private across the DOM renderer and tooltip implementation. The POC had to reproduce those policies. A supported package must extract the pure behavior and make both hosts consume it.
- Shared focus-layer groups are hidden from the base native scene so they are not painted permanently. The proof uses its generic native overlay instead; authored focus marks and inline mark states need a shared scene-state resolver before they can be supported accurately.
- The POC does not implement the complete
tooltip.itemschannel, datum, and derived-row contract. Customcontent,format,formatGroup, and a custom React body work, but default structured content is only partial. - The shared definition type accepts environment-neutral extension tokens, so
a web tooltip token typechecks in native application code and fails when the
native
Chartresolves it. Compile-time host rejection would require a refined native definition type before release. - The root
Viewcurrently claims touch responder ownership on press.ScrollView, pan, brush, zoom, and application gestures require an explicit arbitration model, likely through Pressability or Gesture Handler rather than more responder callbacks. - Native accessibility cannot reuse DOM focus and ARIA. The adjustable-root behavior proves a useful minimum, not parity with keyboard traversal or a complete accessible data representation.
- The chart is one accessible root. Nested controls in a custom pinned tooltip
may be hidden by platform accessibility grouping, and
accessibilityLiveRegionis Android-specific. This needs VoiceOver and TalkBack design rather than prop translation. - A local absolute tooltip cannot escape clipped native ancestors. Native portal/modal behavior needs a separate definition from the browser top-layer implementation.
No existing public call needs to break for an SVG-native release.
Prerequisite changes delivered by the universal-barrel PR:
- additive
@tanstack/charts/universalexport; - additive
@tanstack/charts/typesexport, including generic tooltip and portal token contracts; - mechanical split between universal and DOM host types, with root re-exports preserved.
This proof adds a separate React Native package, base component contract, and optional tooltip subpath.
Additive changes likely required before a supported release:
- a shared, renderer-neutral interaction state module;
- a shared tooltip content/anchor/placement model;
- a runtime option for platform default themes;
- a complete typography and text-measurement contract;
- structured paint references or a host paint-resolution contract;
- native-specific tooltip overlay and gesture integration points.
Changes to avoid:
- making the current
ChartRendererunion over DOM and native surfaces; - making string prerendering optional inside that renderer contract;
- importing React Native from the core package;
- shipping CSS tokens to native and silently substituting arbitrary colors;
- claiming compatibility outside the tested Expo 57 and bare React Native
0.86.2 lanes or the
react-native-svg15.15.4–15.15.5 pair; - adding Skia before an on-device density/performance crossover is measured.
The root type API remains compatible. Consumers that want a DOM-free
declaration graph can opt into @tanstack/charts/types. Shared definitions can
replace browser-root imports with @tanstack/charts/universal; granular entries
remain the bundle-sensitive option. Neither path changes existing published
web behavior.
The package now ships compiled ESM and declarations for the root and optional
tooltip entries. Published exports put types first, select explicit native
shims through the react-native condition, and retain import fallbacks. The
release gate builds and packs the adapter with the fixed package set, then
installs the core and native tarballs into isolated bare React Native and Expo
consumers. Their default Metro configurations must resolve installed dist
files rather than workspace source.
- Most data-to-geometry behavior remains one implementation.
- Existing definitions, accessors, D3 scales, custom marks, polar charts, and geographic paths remain usable.
- The direct scene visitor is small and avoids an SVG XML parse/reconcile step.
react-native-svgprovides one renderer across iOS and Android and supports the current React Native architecture.- Native callbacks receive the original datum objects because the scene and interaction model stay in one JavaScript runtime.
- The separate package keeps native dependencies and platform assumptions out of web bundles.
- Rendering parity is only the easy half; text, input, tooltips, animation, accessibility, and export are host products.
react-native-svgadds both JavaScript and native binary code.- Many SVG nodes can become expensive before the shared scene compiler does.
- Native support needs devices, release builds, TalkBack/VoiceOver testing, and two platform-specific failure surfaces.
- Current interaction and tooltip ownership must be refactored out of mature DOM host implementations without regressing web behavior.
- The native package needs its separate packed-declaration program because React Native and DOM global declarations conflict in one TypeScript program.
- 22 focused POC tests cover every scene primitive, gradients, clips, paint resolution, focus modes, point and callback restoration, tooltip ordering, placement, chart compilation, extension ownership, inactive focus layers, and no-speculative-size behavior. Component mapping tests use React DOM with native modules mocked; they are structural tests, not native renderer tests.
- The native package and the normal React Native application configuration typecheck.
- Packed root and tooltip declarations typecheck in isolated bare and Expo
consumers. Normal and
react-nativeconditional resolution both select the installeddistfiles. - The repository-wide typecheck passes with the native dependency graph.
- The prerequisite PR's packed gate validates the supported environment-safe entries without changing the browser root.
- Existing locked web bundle baselines remain unchanged.
- Production Metro bundles complete for iOS and Android, and the full-chart
variants must resolve
/universal. - Packed bare React Native and Expo consumers produce iOS and Android bundles without workspace aliases. Their source maps include the native conditional entries and exclude workspace source and guarded browser modules.
- The workspace Expo 57 fixture boots in Expo Go on an iOS simulator and renders its axes and line. Expo logs identify the Hermes engine, bytecode transform, and running application. The packed artifact is compiled separately; this smoke does not claim that the tarball itself ran in the simulator.
- Source-map gates prove the full native entries do not include guarded browser implementation files, including the web tooltip and portal runtimes.
- Retained-input gates prove the base native host and line consumer omit the optional native tooltip implementation, all native entries omit web tooltip and portal code, the base host retains no D3 runtime, and the line consumer retains no D3 geometry runtime.
A strict native consumer with skipLibCheck: false now gets past the Charts
DOM boundary and stops in @types/d3-array, whose blurImage declaration
references the browser global ImageData. An automated diagnostic accepts
only those two known errors, or a clean result after the upstream declaration
is fixed. The POC does not add a fake global or pull DOM libraries into native
to hide the upstream declaration problem.
The isolated esbuild boundary and packed bare/Expo Metro gate run in
pnpm validate. GitHub also runs the source native declaration check as a
separate workflow step. The ten-build comparative Metro measurement remains a
local spike command rather than a mandatory CI gate.
Not performed:
- bare React Native or Android simulator rendering, and physical-device rendering;
- native component interaction tests;
- visual comparison against the conformance corpus;
- Hermes release-mode execution or profiling;
- 1k/10k-point interaction and memory tests;
- release application binary comparison;
- VoiceOver and TalkBack validation;
- Expo prebuild or dev-client validation;
- motion, export, or Skia implementation.
Extract interaction and tooltip policy, add native theme ownership, complete the typography contract, and replace the proof's responder policy. Maintain the native-specific package gate and keep the package scoped to tested RN/RNSVG lanes.
Estimated effort: 3–5 engineering weeks.
Add generated native projects for bare and Expo applications, iOS and Android release builds, a platform-neutral definition corpus, screenshot/geometry comparison, VoiceOver/TalkBack flows, binary-size measurement, and sustained interaction benchmarks.
Estimated effort: 3–5 engineering weeks.
Add renderer-neutral motion, native export/share, advanced gesture recipes, and a Skia renderer only if measured SVG node pressure justifies it. Expand the supported version matrix from evidence.
Estimated effort: 4–10 engineering weeks, strongly dependent on motion, accessibility, and dense-data requirements.
An honest SVG-native MVP is therefore roughly 6–10 weeks. Broad parity with the current web product is closer to 10–20 weeks. These are engineering ranges, not schedule commitments.
Proceed to Phase 1 if a real React Native product is planned. The architecture is sound and the JavaScript cost is reasonable for a charting package. Keep the published package experimental until the device-confidence work is complete.
Do not start a NativeScript adapter in parallel. The earlier native platform support spike remains the architecture comparison; this proof increases confidence specifically for React Native because its runtime, Metro pipeline, SVG implementation, and interaction surface were exercised.