Live example:
app/demo/scrubbing.tsx
in the example app.Scrub a crosshair across history
scrub={false}, or configure the crosshair and tooltip:
Keeping scrubs inside the plot
SetclampToPlot to keep the crosshair out of the Y-axis gutter and other
horizontal padding:
onGestureStart. A gesture that starts within its horizontal bounds stays
active when the pointer moves beyond either edge; its crosshair remains clamped
to the nearest plot edge. Scrub-action gestures retain their existing behavior.
The option works on LiveChart and LiveChartSeries and defaults to false
for backwards compatibility.
Customizing the tooltip
The default pill shows the value on the first row and the time on the second, offset to the side of the crosshair. Reshape it withScrubConfig knobs — no custom rendering required:
tooltipPlacement—"side"(default) offsets the pill to the right of the scrub line and flips it left near the right edge."top"/"bottom"center it horizontally over the line, clamped into the plot and pinned to the plot’s top or bottom."point"centers it over the line and floats it just above the scrub dot, flipping below when there isn’t room above (single-series line mode; candle mode keeps its top-pinned OHLC stack).tooltipMargin— the gap (px) between the pill and the plot edge it’s pinned to (the top for"side"/"top", the bottom for"bottom") — or, for"point", the gap between the scrub dot and the pill. Default8. Applies to a customrenderTooltiptoo.tooltipShowValue/tooltipShowTime— both defaulttrue. Drop a row to show just the date (tooltipShowValue: false) or just the value. Turning both off keeps the time row.tooltipBorderRadius,tooltipBackground,tooltipColor,tooltipBorderColor— the pill’s shape and colors.
Custom tooltip (renderTooltip)
For full control — your own layout, blur, or any non-Skia view — pass renderTooltip. It’s the same
idea as renderMarker: return a React
Native element and the chart floats it over the canvas, positioning it on the UI thread (honoring
tooltipPlacement). When set, it replaces the built-in pill entirely.
The catch the JS-thread onScrub can’t solve: the value/date change every frame while scrubbing, so
rebuilding the tooltip in React lags. renderTooltip receives the live fields as
SharedValues — bind them to an animated TextInput
(useAnimatedProps) and both the position and the text update on the UI thread, with no
re-renders.
renderTooltip replaces the built-in pill entirely while scrubbing. Returning
null/undefined from a frame renders nothing for that frame (handy to hide the tooltip in
certain states) — it does not restore the built-in pill. The tooltip* style props above are
ignored while a custom tooltip is active, since you own the chrome.
Custom tooltip in candle mode
renderTooltip works in candle mode too — it replaces the built-in OHLC stack. The context’s
candle field is a SharedValue<CandlePoint | null>
holding the scrubbed bucket, so you can render your own OHLC readout. Bind it the same way — read it
inside useAnimatedProps so the values update on the UI thread:
candle is always present (a SharedValue); it’s null in line mode or when scrubbing is inactive.
value / valueStr are the candle’s close, and time / timeStr the bucket time — so a tooltip
that only shows the close + date needs no candle-specific code at all. This is handy when the active
candle is shown elsewhere in your UI and you want a minimal (or empty) in-chart tooltip.
Trailing fade (dimOpacity)
While scrubbing, the chart content to the right of the crosshair (the “future”) is faded so the
part you’re inspecting stands out. This is done by erasing the trailing content’s alpha — it reveals
the real background, so it works on any background color (no need to match a mask color).
dimOpacity sets the opacity of that trailing region: 1 disables the fade, 0 fully hides it.
Default 0.3.
crosshairDimColor is the legacy alternative: a solid (usually semi-transparent) color painted
over the trailing region. Because it’s a mask on top, it only looks right when it matches the
background — prefer dimOpacity. When crosshairDimColor is set, it overrides the fade.dimOpacity option works on LiveChartSeries.
Hide overlays while scrubbing (hideOverlaysOnScrub)
dimOpacity fades the line/candles, but annotation overlays — buy/sell markers and
reference lines — stay at full strength and can clutter the crosshair read-out. Set
hideOverlaysOnScrub to fade them out for the duration of the scrub and back in on release.
renderMarker /
renderReferenceLine views. The fade is driven by the scrub-active state (not the crosshair’s
edge-proximity fade, so the overlays don’t resurface as the crosshair nears the live dot) and eased
on the UI thread.
Cheap by construction: only a group opacity animates — the marker atlas and reference-line
geometry are untouched (still one batched draw each). It does not rebuild or empty the overlay
data per scrub, so there’s no per-scrub layout/recompute cost.
LiveChartSeries.
Dashed crosshair (crosshairDash)
The vertical crosshair line is solid by default. Set crosshairDash to draw it dashed — true
applies a default [4, 4] dash, or pass explicit Skia dash intervals [on, off, …] in pixels for
finer control. Omitting it (or false) keeps the solid line.
crosshairLineColor for the stroke color, and works on LiveChartSeries too.
Crosshair width, line cap, overshoot, and edge fade
UsecrosshairStrokeWidth to change the line width and crosshairOvershoot to extend it past the
plot’s top and bottom edges. Both values are pixels; their defaults are 1 and 0. Negative
overshoot values are clamped to 0. A custom top tooltip’s measured line stop is preserved so
overshoot never draws the crosshair through the tooltip.
Set crosshairLineCap to "butt", "round", or "square" to control the ends of the vertical
line. Omitting it preserves the existing Skia default. Caps compose with solid, thick, and dashed
crosshairs.
The crosshair fades over the final 4 px as it approaches the live edge by default.
crosshairFadeDistance changes that distance for the visible line, selection dot, and tooltip;
negative values clamp to 0. Set crosshairFade: false to keep that visible group fully opaque
while scrubbing. Neither option changes the trailing-content dim’s existing edge fade.
LiveChartSeries.
Press-and-hold to scrub (panGestureDelay)
By default scrubbing starts the moment the user drags. When the chart sits inside a horizontally
swipeable container — most commonly a stack navigator with swipe-back-to-previous-route — that
can steal the back gesture. panGestureDelay makes scrubbing a press and hold: the pan only
activates after the finger is held for that many milliseconds, so a quick horizontal flick falls
through to the parent gesture, while a deliberate hold begins scrubbing.
0 (the default) scrubs immediately. Works the same on LiveChartSeries.
Selection dot
A dot is drawn at the scrub intersection — where the crosshair meets the line. OnLiveChart it’s
on by default, so you get it for free with scrub. On LiveChartSeries it’s off by
default — with multiple lines the dot can only track the leading series, so the crosshair +
the opt-in per-series tooltip dots mark the scrub point instead; pass selectionDot explicitly
to opt in.
Turn it off, or tweak its look, with the boolean | SelectionDotConfig selectionDot prop:
component — a Skia dot that receives the scrub position as
SharedValues (SelectionDotProps), so it animates on the UI thread without re-renders. When set,
the size / color / ring knobs are ignored.
selectionDot prop works on LiveChartSeries — there it defaults to hidden, so pass
true (or a config) to show the dot anchored to the leading series.
Reading the scrub position (single series)
onScrub fires with a ScrubPoint while scrubbing, and null when it ends. It runs on the JS
thread, so you can call React setState directly:
Gesture lifecycle (onGestureStart / onGestureEnd)
onGestureStart and onGestureEnd fire once at the edges of a scrub — on both LiveChart and
LiveChartSeries. Unlike onScrub, they’re plain JS-thread functions (not worklets), so a
regular useState is fine:
onGestureStart fires when the pan activates — so it respects scrub.panGestureDelay and only
fires once the press-and-hold threshold is met. onGestureEnd fires only if a scrub actually
started; a quick tap that never activates emits neither callback.
They pair well with side effects you want to bracket around an interaction — fire a haptic on start,
or pause a live feed while the user inspects history and resume it on end.
Multi-series scrub (worklet)
ForLiveChartSeries, onScrub is a worklet that runs on the UI thread each frame with a
ScrubPointMulti (including a per-series value array). Write straight to a shared value with no
bridge overhead:
point.seriesValues is an array of { id, label, value } for each visible series at the scrub
time. See the types reference for the full payload shape.
Order ticket (scrubAction)
scrubAction turns the ephemeral crosshair into a placed interaction — tap to drop a locked price reticle, drag to fine-tune, then press to fire an order callback. It has its own guide: Order ticket.