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

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

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

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

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

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