> ## Documentation Index
> Fetch the complete documentation index at: https://react-native-livechart.brandtnewlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Scrubbing

> Pan a crosshair across history and read the value under it.

Scrubbing is on by default. Drag across the chart to reveal a vertical crosshair, the content
ahead of it faded back, and a tooltip pill with the value and time under your finger.

<Note>
  **Live example:**
  [`app/demo/scrubbing.tsx`](https://github.com/brandtnewlabs/react-native-livechart/blob/main/app/demo/scrubbing.tsx)
  in the example app.
</Note>

<Frame caption="Scrub a crosshair across history">
  <video autoPlay loop muted playsInline controls poster="/media/scrubbing.jpg" src="https://mintcdn.com/brandtnewlabs/3aK_iydbC8Ff9g3s/media/scrubbing.mp4?fit=max&auto=format&n=3aK_iydbC8Ff9g3s&q=85&s=0bc8dc7c39f55a514dfa87d7cd17db42" data-path="media/scrubbing.mp4" />
</Frame>

```tsx theme={null}
<LiveChart data={data} value={value} scrub />
```

Disable it with `scrub={false}`, or configure the crosshair and tooltip:

```tsx theme={null}
<LiveChart
  data={data}
  value={value}
  scrub={{ tooltip: true, crosshairLineColor: "#94a3b8" }}
/>
```

## Keeping scrubs inside the plot

Set `clampToPlot` to keep the crosshair out of the Y-axis gutter and other
horizontal padding:

```tsx theme={null}
<LiveChart
  data={data}
  value={value}
  scrub={{ clampToPlot: true }}
/>
```

A plain-scrub gesture that starts left or right of the plot is rejected before
activation, remains available to competing or parent gestures, and does not
fire `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.

## Snap to candle centers (`snapToCandles`)

In candle mode the crosshair glides continuously under the finger by default. Set
`snapToCandles` to quantize the scrub X to the hovered candle's center: the crosshair — and
everything derived from it (the time, tooltip, and trailing dim) — jumps from candle to candle
instead, the tick-to-tick feel of pro charting tools:

```tsx theme={null}
<LiveChart
  data={data}
  value={value}
  mode="candle"
  candles={candles}
  liveCandle={liveCandle}
  candleWidth={60}
  scrub={{ snapToCandles: true }}
/>
```

A position in a gap between candles keeps the raw finger X, and the option is a no-op in line
mode. Defaults to `false`. The example app's **Candlestick** screen includes a **Snap to
candles** switch to compare both behaviors.

## 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 with `ScrubConfig` knobs — no custom rendering required:

```tsx theme={null}
<LiveChart
  data={data}
  value={value}
  scrub={{
    tooltipPlacement: "top",     // "side" (default) | "top" | "bottom" | "point"
    tooltipShowValue: false,     // drop the value row → date-only pill
    tooltipBorderRadius: 12,     // pill corner radius; default 5
    tooltipBackground: "#1e293b",
    tooltipColor: "#e2e8f0",
    tooltipBorderColor: "#334155",
  }}
/>
```

* **`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. Default `8`. Applies to a custom `renderTooltip` too.
* **`tooltipShowValue` / `tooltipShowTime`** — both default `true`. 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`](/guides/markers-and-trades#custom-marker-rendering): 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
[`SharedValue`s](/api-reference/types#tooltiprenderprops) — bind them to an animated `TextInput`
(`useAnimatedProps`) and both the position **and** the text update on the UI thread, with no
re-renders.

```tsx theme={null}
import { View, TextInput, StyleSheet } from "react-native";
import Animated, { useAnimatedProps } from "react-native-reanimated";
import type { TooltipRenderProps } from "react-native-livechart";

const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);

function BlueTooltip({ timeStr }: TooltipRenderProps) {
  // Animate the date text on the UI thread — no JS re-render while scrubbing.
  const animatedProps = useAnimatedProps(() => {
    const t = timeStr.get();
    return { text: t, defaultValue: t };
  });
  return (
    <View style={styles.pill}>
      <AnimatedTextInput editable={false} style={styles.text} animatedProps={animatedProps} />
    </View>
  );
}

const styles = StyleSheet.create({
  pill: {
    borderRadius: 10,
    backgroundColor: "#3323E6",
    paddingHorizontal: 12,
    paddingVertical: 6,
  },
  // Fixed width: the TextInput is measured once at mount, and UI-thread text
  // updates don't re-trigger a layout pass — an auto-width pill would clip.
  // Use a monospace font (or `tabular-nums`) so every digit is the same width
  // and the value never outgrows the fixed pill as it changes.
  text: {
    width: 72,
    textAlign: "center",
    color: "#fff",
    fontSize: 13,
    padding: 0,
    fontVariant: ["tabular-nums"],
  },
});

<LiveChart data={data} value={value} scrub renderTooltip={BlueTooltip} />;
```

Supplying `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`](/api-reference/types#tooltiprenderprops) 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:

```tsx theme={null}
function OhlcTooltip({ candle }: TooltipRenderProps) {
  const animatedProps = useAnimatedProps(() => {
    const c = candle.get();
    const text = c
      ? `O ${c.open.toFixed(2)}  H ${c.high.toFixed(2)}\nL ${c.low.toFixed(2)}  C ${c.close.toFixed(2)}`
      : "";
    return { text, defaultValue: text };
  });
  return (
    <View style={styles.ohlcPill}>
      <AnimatedTextInput editable={false} multiline style={styles.ohlcText} animatedProps={animatedProps} />
    </View>
  );
}

<LiveChart mode="candle" candles={candles} liveCandle={liveCandle} scrub renderTooltip={OhlcTooltip} />;
```

`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.

## Scrub dimming (`dimTarget`, `dimOpacity`)

By default, scrubbing fades chart content to the **right** of the crosshair (the "future") so the
part you're inspecting stands out. Set `dimTarget: "otherCandles"` in candle mode to keep the
candle under the finger at full strength while fading every other candle body and wick:

```tsx theme={null}
<LiveChart
  mode="candle"
  candles={candles}
  liveCandle={liveCandle}
  scrub={{
    tooltip: false,
    snapToCandles: true,
    dimTarget: "otherCandles",
    dimOpacity: 0.5,
    dimFadeMs: 60,
  }}
/>
```

`dimOpacity` is clamped to `[0, 1]`: `1` disables the fade and `0` fully hides the selected target.
Default `0.3`. With `"otherCandles"`, volume bars stay unchanged and gaps select no candle. The
vertical crosshair and scrub selection dot are hidden so the focused candle is the sole position
indicator. If the chart switches to line mode while keeping this config, scrubbing falls back to the
standard guide, selection dot, and `"future"` fade. The default `dimTarget: "future"` preserves the
existing trailing fade.

`dimFadeMs` controls how quickly the other candles ease toward `dimOpacity` and return to full
strength on release. It defaults to `60` ms; set it to `0` for the previous instant change. The
selected candle remains fully opaque while the surrounding batch animates.

The trailing fade erases content alpha, revealing the **real background**, so it works on any
background color (no need to match a mask color).

<Note>
  `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.
</Note>

The default `dimTarget: "future"` and its `dimOpacity` work on `LiveChartSeries`;
`"otherCandles"` focus rendering is candle-mode only.

## 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.

```tsx theme={null}
<LiveChart data={data} value={value} scrub={{ hideOverlaysOnScrub: true }} />
```

It covers both the built-in Skia tags/lines **and** any custom `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.

<Note>
  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.
</Note>

The leading live dot is governed separately — see [Selection dot](#selection-dot). Works the same on
`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.

```tsx theme={null}
<LiveChart
  data={data}
  value={value}
  scrub={{ crosshairLineColor: "#94a3b8", crosshairDash: [3, 4] }}
/>
```

Pairs with `crosshairLineColor` for the stroke color, and works on `LiveChartSeries` too.

## Crosshair width, line cap, overshoot, and edge fade

Use `crosshairStrokeWidth` 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.

```tsx theme={null}
<LiveChart
  data={data}
  value={value}
  scrub={{
    crosshairStrokeWidth: 2,
    crosshairLineCap: "round",
    crosshairOvershoot: 6,
    crosshairFadeDistance: 12,
  }}
/>
```

These options also work on `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.

```tsx theme={null}
// Hold ~250ms to scrub; a quick swipe navigates back instead.
<LiveChart data={data} value={value} scrub={{ panGestureDelay: 250 }} />
```

`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. On `LiveChart` 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:

```tsx theme={null}
// Hide the dot
<LiveChart data={data} value={value} selectionDot={false} />

// Bigger amber dot, slightly thicker halo
<LiveChart
  data={data}
  value={value}
  selectionDot={{ size: 6, color: "#fbbf24", ring: { width: 2 } }}
/>

// Flat dot, no outer ring
<LiveChart data={data} value={value} selectionDot={{ ring: false }} />
```

For full control, pass a `component` — a Skia dot that receives the scrub position as
`SharedValue`s (`SelectionDotProps`), so it animates on the UI thread without re-renders. When set,
the `size` / `color` / `ring` knobs are ignored.

```tsx theme={null}
import { Circle, Group } from "@shopify/react-native-skia";
import type { SelectionDotProps } from "react-native-livechart";

function RingDot({ x, y, opacity, color, size }: SelectionDotProps) {
  return (
    <Group opacity={opacity}>
      <Circle cx={x} cy={y} r={size + 3} color={color} style="stroke" strokeWidth={2} />
      <Circle cx={x} cy={y} r={size - 1} color={color} />
    </Group>
  );
}

<LiveChart data={data} value={value} selectionDot={{ component: RingDot }} />;
```

The same `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:

```tsx theme={null}
<LiveChart
  data={data}
  value={value}
  onScrub={(point) => {
    if (!point) return; // scrub ended
    console.log(point.time, point.value, point.x, point.y);
    // In candle mode, point.candle holds the OHLC under the crosshair.
  }}
/>
```

## 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:

```tsx theme={null}
import { useState } from "react";

const [scrubbing, setScrubbing] = useState(false);

<LiveChart
  data={data}
  value={value}
  onGestureStart={() => setScrubbing(true)}
  onGestureEnd={() => setScrubbing(false)}
/>;
```

`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)

For `LiveChartSeries`, `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:

```tsx theme={null}
import { useSharedValue } from "react-native-reanimated";
import type { ScrubPointMulti } from "react-native-livechart";

const scrubData = useSharedValue<ScrubPointMulti | null>(null);

<LiveChartSeries
  series={series}
  scrub
  onScrub={(point) => {
    "worklet";
    scrubData.set(point); // null when scrub ends
  }}
/>;
```

`point.seriesValues` is an array of `{ id, label, value }` for each visible series at the scrub
time. See the [types reference](/api-reference/types) 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**](/guides/order-ticket).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.