ChartCrosshairNew

A vertical rule and tooltip overlay that highlight the pressed point on a chart.

Import

import { ChartCrosshair } from 'heroui-native-pro';

Anatomy

<ChartCrosshair.Anchor chartBounds={...} isActive={...} x={...}>
  <SomeHeroUINative.Chart>
    {({ chartBounds }) => (
      <ChartCrosshair x={...} top={chartBounds.top} bottom={chartBounds.bottom} />
    )}
  </SomeHeroUINative.Chart>
  <ChartCrosshair.Value value={...}>
    <ChartCrosshair.ValueLabel />
  </ChartCrosshair.Value>
</ChartCrosshair.Anchor>
  • ChartCrosshair: Skia vertical rule (Path) drawn from (x, top) to (x, bottom). Renders dashed by default via DashPathEffect; pass variant="solid" for an unbroken stroke. Render inside the chart canvas with useChartPressState-driven shared values.
  • ChartCrosshair.Anchor: Relatively positioned React Native View that wraps the chart and the sibling RN value overlay. Supplies crosshair context (x, isActive, chartBounds) so descendants can position themselves on the same coordinate system as the Skia rule.
  • ChartCrosshair.Value: Absolutely positioned animated overlay that hosts the tooltip pill. Measures its own width to center on x, clamps to chartBounds, and tracks press activity via isActive opacity. Must be a descendant of ChartCrosshair.Anchor.
  • ChartCrosshair.ValueLabel: Read-only animated label backed by an internal ReText (read-only Reanimated TextInput). Reads the value shared string from ChartCrosshair.Value context, so the label updates on the UI thread without React renders.

Usage

When wrapping a chart with ChartCrosshair.Anchor, the chart's wrapperClassName must not contain padding (e.g. p-*, px-*, py-*). The anchor reads chartBounds in the same coordinate space as the Skia canvas, so any padding on the wrapper offsets the chart relative to the anchor and breaks centering / clamping of ChartCrosshair.Value. Apply spacing on a parent container instead.

Basic usage

Render ChartCrosshair inside the chart's render callback. Drive x from useChartPressState, and pass top / bottom from chartBounds. Gate visibility with isActive from the same hook.

const { state, isActive } = useChartPressState({
  x: '' as string,
  y: { revenue: 0 },
});

<LineChart
  data={DATA}
  xKey="month"
  yKeys={['revenue']}
  chartPressState={state}
  wrapperClassName="h-48"
>
  {({ points, chartBounds }) => (
    <>
      <LineChart.Line points={points.revenue} />
      {isActive ? (
        <ChartCrosshair
          x={state.x.position}
          top={chartBounds.top}
          bottom={chartBounds.bottom}
        />
      ) : null}
    </>
  )}
</LineChart>;

Variants

Switch the rule style with the variant prop. dashed attaches a themed DashPathEffect; solid renders an unbroken stroke.

<ChartCrosshair x={state.x.position} top={chartBounds.top} bottom={chartBounds.bottom} variant="dashed" />
<ChartCrosshair x={state.x.position} top={chartBounds.top} bottom={chartBounds.bottom} variant="solid" />

Custom dash pattern

Override the dashed pattern by nesting your own Skia DashPathEffect as a child.

import { DashPathEffect } from '@shopify/react-native-skia';

<ChartCrosshair
  x={state.x.position}
  top={chartBounds.top}
  bottom={chartBounds.bottom}
>
  <DashPathEffect intervals={[6, 3]} />
</ChartCrosshair>;

Custom color and stroke width

Pass color and strokeWidth directly to override the themed defaults.

<ChartCrosshair
  x={state.x.position}
  top={chartBounds.top}
  bottom={chartBounds.bottom}
  color="#8b5cf6"
  strokeWidth={2}
/>

Tooltip overlay

Wrap the chart and the value pill in ChartCrosshair.Anchor, then render ChartCrosshair.Value as a sibling outside the chart. Build the label string on the UI thread with useDerivedValue and pass it as value. Mirror Skia chartBounds from onChartBoundsChange so the overlay clamps correctly near the plot edges.

Keep wrapperClassName free of padding on the wrapped chart — the anchor measures positions in the chart's native coordinate space.

const { state, isActive } = useChartPressState({
  x: '' as string,
  y: { revenue: 0 },
});

const [chartBounds, setChartBounds] = useState<ChartBounds | null>(null);

const labelText = useDerivedValue(() => `${state.y.revenue.value.get()}`);

<ChartCrosshair.Anchor
  chartBounds={chartBounds ?? undefined}
  isActive={state.isActive}
  x={state.x.position}
>
  <LineChart
    data={DATA}
    xKey="month"
    yKeys={['revenue']}
    chartPressState={state}
    onChartBoundsChange={setChartBounds}
    wrapperClassName="h-[200px]"
  >
    {({ points, chartBounds: bounds }) => (
      <>
        <LineChart.Line points={points.revenue} />
        {isActive ? (
          <ChartCrosshair
            x={state.x.position}
            top={bounds.top}
            bottom={bounds.bottom}
          />
        ) : null}
      </>
    )}
  </LineChart>
  <ChartCrosshair.Value value={labelText} />
</ChartCrosshair.Anchor>;

Value variants

Switch the pill surface with the variant prop on ChartCrosshair.Value. default renders a filled rounded pill; ghost renders a transparent label-only surface.

<ChartCrosshair.Value value={labelText} variant="default" />
<ChartCrosshair.Value value={labelText} variant="ghost" />

Value placement

Position the pill above (top) or below (bottom) the anchor with the placement prop.

<ChartCrosshair.Value value={labelText} placement="top" />
<ChartCrosshair.Value value={labelText} placement="bottom" />

Value offset

Nudge the overlay without fighting the animated style. offset accepts CSS-like additive top / bottom / left / right pixels.

The animated style owns vertical edge (top / bottom) and horizontal transform.translateX, so do not override those via className or styles.container. Use offset instead.

<ChartCrosshair.Value
  value={labelText}
  placement="top"
  offset={{ top: 8, left: 4 }}
/>

Custom value content

Compose extra content (e.g. icons, prefixes) by passing children. The default label is replaced by the children — render ChartCrosshair.ValueLabel explicitly to keep the animated text alongside your custom nodes.

<ChartCrosshair.Value value={labelText}>
  <View className="flex-row items-center gap-1 px-2 py-1">
    <DollarIcon />
    <ChartCrosshair.ValueLabel />
  </View>
</ChartCrosshair.Value>

Example

import { Card } from 'heroui-native';
import { ChartCrosshair, ChartIndicator, LineChart } from 'heroui-native-pro';
import { useState } from 'react';
import { View } from 'react-native';
import { useDerivedValue } from 'react-native-reanimated';
import type { ChartBounds } from 'victory-native';
import { useChartPressState } from 'victory-native';

const REVENUE_DATA = [
  { month: 'Jan', revenue: 4200 },
  { month: 'Feb', revenue: 5800 },
  { month: 'Mar', revenue: 4900 },
  { month: 'Apr', revenue: 7200 },
  { month: 'May', revenue: 6100 },
  { month: 'Jun', revenue: 8400 },
  { month: 'Jul', revenue: 7800 },
  { month: 'Aug', revenue: 9200 },
  { month: 'Sep', revenue: 8600 },
  { month: 'Oct', revenue: 10200 },
  { month: 'Nov', revenue: 9800 },
  { month: 'Dec', revenue: 11500 },
];

const formatThousandsCurrency = (value: number): string =>
  `$${(value / 1000).toFixed(0)}k`;

export default function CrosshairChartExample() {
  const { state, isActive } = useChartPressState({
    x: '' as string,
    y: { revenue: 0 },
  });

  const [chartBounds, setChartBounds] = useState<ChartBounds | null>(null);

  const tooltipLabel = useDerivedValue(() => `${state.y.revenue.value.get()}`);

  return (
    <View className="flex-1 w-full px-5 justify-center">
      <Card>
        <Card.Header className="mb-10 gap-1">
          <Card.Title className="text-sm">Monthly Revenue</Card.Title>
        </Card.Header>
        <Card.Body>
          <ChartCrosshair.Anchor
            chartBounds={chartBounds ?? undefined}
            isActive={state.isActive}
            x={state.x.position}
          >
            <LineChart
              data={REVENUE_DATA}
              xKey="month"
              yKeys={['revenue']}
              chartPressState={state}
              yAxis={[{ formatYLabel: formatThousandsCurrency }]}
              wrapperClassName="h-[200px]"
              onChartBoundsChange={setChartBounds}
            >
              {({ points, chartBounds: bounds }) => (
                <>
                  <LineChart.Line
                    points={points.revenue}
                    curveType="monotoneX"
                  />
                  {isActive ? (
                    <>
                      <ChartCrosshair
                        bottom={bounds.bottom}
                        top={bounds.top}
                        x={state.x.position}
                      />
                      <ChartIndicator
                        x={state.x.position}
                        y={state.y.revenue.position}
                      />
                    </>
                  ) : null}
                </>
              )}
            </LineChart>
            <ChartCrosshair.Value value={tooltipLabel} />
          </ChartCrosshair.Anchor>
        </Card.Body>
      </Card>
    </View>
  );
}

API Reference

ChartCrosshair

proptypedefaultdescription
xSharedValue<number>-Horizontal position of the rule, typically state.x.position from useChartPressState
topnumber-Top y-coordinate (Skia canvas pixels) where the rule starts. Typically chartBounds.top
bottomnumber-Bottom y-coordinate (Skia canvas pixels) where the rule ends. Typically chartBounds.bottom
variantChartCrosshairVariant'dashed'Visual style of the rule. 'dashed' attaches a themed DashPathEffect
colorColor-Skia stroke color. Falls back to a themed muted color when omitted
strokeWidthnumber1Stroke width in logical pixels
childrenReactNode-Optional Skia children (e.g. a custom DashPathEffect) to nest inside the Path
...SkiaPathPropsComponentProps<typeof Path>-Remaining Skia Path props. path, style, start, and end are controlled internally

ChartCrosshairVariant

typedescription
'solid' | 'dashed'Visual style of the rule. 'dashed' attaches a themed dash effect

ChartCrosshair.Anchor

proptypedefaultdescription
childrenReactNode-The chart and sibling ChartCrosshair.Value overlay
chartBoundsChartBounds-Plot bounds mirrored from the chart's onChartBoundsChange. Enables horizontal clamping
isActiveSharedValue<boolean>-Press activity shared value used to drive overlay opacity
xSharedValue<number>-Horizontal crosshair position in chart space (state.x.position)
...ViewPropsViewProps-All standard React Native View props are supported

ChartCrosshair.Value

proptypedefaultdescription
valueSharedValue<string>-Shared label string forwarded to ChartCrosshair.ValueLabel via context
variantChartCrosshairValueVariant'default'Visual variant for the pill container
placementChartCrosshairValuePlacement'top'Whether the pill sits above ('top') or below ('bottom') the anchor
offsetChartCrosshairValueOffset-Pixel offsets applied on top of the auto-centering animated style. CSS-like additive top/bottom/left/right
classNamestring-Additional classes merged onto the container slot
classNamesElementSlots<ValueSlots>-Additional classes per slot (container, label)
stylesChartCrosshairValueStyles-Inline style overrides per slot
childrenReactNode-Optional content rendered after (or replacing) the default label
...ViewPropsOmit<ViewProps, 'children'>-All standard React Native View props are supported except children (typed above)

ChartCrosshairValueVariant

typedescription
'default' | 'ghost'Pill surface variant. 'ghost' removes background and pad

ChartCrosshairValuePlacement

typedescription
'top' | 'bottom'Vertical placement of the overlay relative to anchor

ChartCrosshairValueOffset

Pixel offsets applied to the animated overlay on top of auto-centering. Values are CSS-like additive — use this prop instead of overriding top / bottom / transform via className or styles, since those properties are owned by the animated style and will be overwritten on every frame.

proptypedefaultdescription
topnumber0Vertical inset that pushes the overlay down (positive)
bottomnumber0Vertical inset that pushes the overlay up (positive)
leftnumber0Horizontal pixel offset added to translateX. Positive values push right
rightnumber0Horizontal pixel offset subtracted from translateX. Positive values push left

ElementSlots<ValueSlots>

slotdescription
containerOuter animated Animated.View that hosts the pill
labelDefault label slot classes merged onto the inner ChartCrosshair.ValueLabel

styles

slottypedescription
containerViewStyleStyle for the animated overlay Animated.View
labelTextStyleStyle for the read-only animated TextInput label

ChartCrosshair.ValueLabel

Reads the animated string from ChartCrosshair.Value context — the value is never a prop. Extra props forward to the underlying ReText / TextInput.

proptypedefaultdescription
classNamestring-Additional classes merged with the default label typography
styleAnimatedProps<TextInputProps>['style']-Animated style for the TextInput / ReText surface
...TextInputPropsOmit<TextInputProps, 'children' | 'defaultValue' | 'style' | 'value'>-All standard TextInput props except the excluded ones

Hooks

useChartCrosshairAnchor

Hook to access the ChartCrosshair.Anchor context. Must be used within a ChartCrosshair.Anchor subtree.

import { useChartCrosshairAnchor } from 'heroui-native-pro';

const { x, isActive, chartBounds } = useChartCrosshairAnchor();

Returns: ChartCrosshairAnchorContextValue

propertytypedescription
xSharedValue<number>Horizontal crosshair position in chart space
isActiveSharedValue<boolean>Press activity shared value (overlay opacity tracks this)
chartBoundsChartBoundsLatest Skia plot bounds, when available

useChartCrosshairValue

Hook to access the ChartCrosshair.Value context. Must be used within a ChartCrosshair.Value subtree.

import { useChartCrosshairValue } from 'heroui-native-pro';

const { value } = useChartCrosshairValue();

Returns: ChartCrosshairValueContextValue

propertytypedescription
valueSharedValue<string>Animated label string from the root value prop

On this page