A vertical rule and tooltip overlay that highlight the pressed point on a chart.
import { ChartCrosshair } from 'heroui-native-pro';<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>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.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.x, clamps to chartBounds, and tracks press activity via isActive opacity. Must be a descendant of ChartCrosshair.Anchor.default variant when the active theme registers default background content (e.g. glass). Replace or remove it via the background prop.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.When wrapping a chart with
ChartCrosshair.Anchor, the chart'swrapperClassNamemust not contain padding (e.g.p-*,px-*,py-*). The anchor readschartBoundsin 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 ofChartCrosshair.Value. Apply spacing on a parent container instead.
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>;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" />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>;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}
/>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
wrapperClassNamefree 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>;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" />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" />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 horizontaltransform.translateX, so do not override those viaclassNameorstyles.container. Useoffsetinstead.
<ChartCrosshair.Value
value={labelText}
placement="top"
offset={{ top: 8, left: 4 }}
/>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>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>
);
}| prop | type | default | description |
|---|---|---|---|
x | SharedValue<number> | - | Horizontal position of the rule, typically state.x.position from useChartPressState |
top | number | - | Top y-coordinate (Skia canvas pixels) where the rule starts. Typically chartBounds.top |
bottom | number | - | Bottom y-coordinate (Skia canvas pixels) where the rule ends. Typically chartBounds.bottom |
variant | ChartCrosshairVariant | 'dashed' | Visual style of the rule. 'dashed' attaches a themed DashPathEffect |
color | Color | - | Skia stroke color. Falls back to a themed muted color when omitted |
strokeWidth | number | 1 | Stroke width in logical pixels |
children | ReactNode | - | Optional Skia children (e.g. a custom DashPathEffect) to nest inside the Path |
...SkiaPathProps | ComponentProps<typeof Path> | - | Remaining Skia Path props. path, style, start, and end are controlled internally |
| type | description |
|---|---|
'solid' | 'dashed' | Visual style of the rule. 'dashed' attaches a themed dash effect |
| prop | type | default | description |
|---|---|---|---|
children | ReactNode | - | The chart and sibling ChartCrosshair.Value overlay |
chartBounds | ChartBounds | - | Plot bounds mirrored from the chart's onChartBoundsChange. Enables horizontal clamping |
isActive | SharedValue<boolean> | - | Press activity shared value used to drive overlay opacity |
x | SharedValue<number> | - | Horizontal crosshair position in chart space (state.x.position) |
...ViewProps | ViewProps | - | All standard React Native View props are supported |
| prop | type | default | description |
|---|---|---|---|
value | SharedValue<string> | - | Shared label string forwarded to ChartCrosshair.ValueLabel via context |
variant | ChartCrosshairValueVariant | 'default' | Visual variant for the pill container |
placement | ChartCrosshairValuePlacement | 'top' | Whether the pill sits above ('top') or below ('bottom') the anchor |
offset | ChartCrosshairValueOffset | - | Pixel offsets applied on top of the auto-centering animated style. CSS-like additive top/bottom/left/right |
className | string | - | Additional classes merged onto the container slot |
classNames | ElementSlots<ValueSlots> | - | Additional classes per slot (container, label) |
styles | ChartCrosshairValueStyles | - | Inline style overrides per slot |
children | ReactNode | - | Optional content rendered after (or replacing) the default label |
background | ReactNode | - | Background layer behind the value pill surface. undefined renders the theme-aware default for the default variant; custom node replaces it; null removes it |
...ViewProps | Omit<ViewProps, 'children'> | - | All standard React Native View props are supported except children (typed above) |
| type | description |
|---|---|
'default' | 'ghost' | Pill surface variant. 'ghost' removes background and pad |
| type | description |
|---|---|
'top' | 'bottom' | Vertical placement of the overlay relative to anchor |
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.
| prop | type | default | description |
|---|---|---|---|
top | number | 0 | Vertical inset that pushes the overlay down (positive) |
bottom | number | 0 | Vertical inset that pushes the overlay up (positive) |
left | number | 0 | Horizontal pixel offset added to translateX. Positive values push right |
right | number | 0 | Horizontal pixel offset subtracted from translateX. Positive values push left |
| slot | description |
|---|---|
container | Outer animated Animated.View that hosts the pill |
label | Default label slot classes merged onto the inner ChartCrosshair.ValueLabel |
| slot | type | description |
|---|---|---|
container | ViewStyle | Style for the animated overlay Animated.View |
label | TextStyle | Style for the read-only animated TextInput label |
Absolute-fill container rendered behind the value pill surface. With no children, the active library theme decides the default content (e.g. a glass blur layer); pass children to host custom content with the same positioning and clipping.
| prop | type | default | description |
|---|---|---|---|
children | ReactNode | - | Custom content inside the background container |
className | string | - | Additional CSS classes |
...ViewProps | ViewProps | - | All standard React Native View props are supported |
Reads the animated string from ChartCrosshair.Value context — the value is never a prop. Extra props forward to the underlying ReText / TextInput.
| prop | type | default | description |
|---|---|---|---|
className | string | - | Additional classes merged with the default label typography |
style | AnimatedProps<TextInputProps>['style'] | - | Animated style for the TextInput / ReText surface |
...TextInputProps | Omit<TextInputProps, 'children' | 'defaultValue' | 'style' | 'value'> | - | All standard TextInput props except the excluded ones |
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();| property | type | description |
|---|---|---|
x | SharedValue<number> | Horizontal crosshair position in chart space |
isActive | SharedValue<boolean> | Press activity shared value (overlay opacity tracks this) |
chartBounds | ChartBounds | Latest Skia plot bounds, when available |
Hook to access the ChartCrosshair.Value context. Must be used within a ChartCrosshair.Value subtree.
import { useChartCrosshairValue } from 'heroui-native-pro';
const { value } = useChartCrosshairValue();| property | type | description |
|---|---|---|
value | SharedValue<string> | Animated label string from the root value prop |