First decide what you actually want
“Tooltip” covers three different things in a mobile app, and picking the wrong one is the most common mistake:
- An on-demand hint. A small bubble that explains one control when the user taps an info icon. This is the classic tooltip and the subject of this post.
- A popover. A bubble with real content: a short menu, a date picker, a confirmation. Same positioning problem, bigger box, different library.
- A coach mark. A sequence of hints with a dimmed backdrop that runs once on first launch. Different problem entirely; use an onboarding library.
One more thing worth saying plainly: a tooltip is never the only way to learn what a control does. Screen reader users will not perform your long-press. Put the same text in accessibilityHint on the control, and treat the tooltip as the visual version of it.
The options, honestly
| Option | Type | Platforms | Pick it for | The cost |
|---|---|---|---|---|
react-native-walkthrough-tooltip | JS, renders in a Modal | iOS · Android · Web | Arrow, auto-placement, dimmed backdrop, works in Expo Go | Content area is small by design; one tooltip open at a time |
react-native-popover-view | JS, renders in a Modal | iOS · Android | Arbitrary children, anchors to a ref or a rect, good dismiss handling | Heavier; placement logic can fight you near screen edges |
react-native-copilot / rn-tourguide | JS, onboarding | iOS · Android | Multi-step walkthrough with spotlight and next/skip for free | Wrong tool for a single on-demand hint |
DIY: Pressable + measureInWindow + Modal | JS, yours | iOS · Android · Web | No dependency, exactly your design, 60 lines | You handle edge flipping and the arrow yourself |
If you want the library answer: react-native-walkthrough-tooltip. It wraps the anchor, measures it, renders the bubble in its own Modal so nothing can clip it, and flips placement when the anchor is near an edge. The API is small:
import Tooltip from 'react-native-walkthrough-tooltip';
function BudgetHint() {
const [open, setOpen] = useState(false);
return (
<Tooltip
isVisible={open}
placement="top"
onClose={() => setOpen(false)}
content={<Text>Rolls over unspent money to next month.</Text>}
>
<Pressable
onPress={() => setOpen(true)}
accessibilityLabel="What does rollover mean"
accessibilityHint="Rolls over unspent money to next month"
hitSlop={8}
>
<Ionicons name="information-circle-outline" size={18} />
</Pressable>
</Tooltip>
);
}Note the hitSlop. An 18-point icon is below the 44-point minimum touch target on both platforms, and a tooltip that is hard to open is a tooltip nobody reads.
A tooltip you own, in 60 lines
For one or two hints in an app, a dependency is more code than the feature. The pattern below is what the libraries do internally: measure the anchor in window coordinates, open a transparent Modal, and place the bubble relative to the measured rect.
// Tooltip.tsx
import { useRef, useState } from 'react';
import { Modal, Pressable, Text, View, StyleSheet, useWindowDimensions } from 'react-native';
type Rect = { x: number; y: number; width: number; height: number };
export function Tooltip({ text, children }) {
const anchor = useRef<View>(null);
const [rect, setRect] = useState<Rect | null>(null);
const { width: screenW } = useWindowDimensions();
const open = () => {
// measureInWindow gives window coords, which is what a Modal uses.
// measure() gives coords relative to the parent, which is useless here.
anchor.current?.measureInWindow((x, y, width, height) => {
setRect({ x, y, width, height });
});
};
const bubbleW = 220;
// Center under the anchor, then clamp so it never leaves the screen.
const left = rect
? Math.min(Math.max(8, rect.x + rect.width / 2 - bubbleW / 2), screenW - bubbleW - 8)
: 0;
return (
<>
<Pressable ref={anchor} onPress={open} hitSlop={8} accessibilityHint={text}>
{children}
</Pressable>
<Modal visible={!!rect} transparent animationType="fade" onRequestClose={() => setRect(null)}>
{/* Full-screen backdrop: a tap anywhere dismisses. */}
<Pressable style={styles.backdrop} onPress={() => setRect(null)}>
{rect && (
<View style={[styles.bubble, { top: rect.y + rect.height + 8, left, width: bubbleW }]}>
<Text style={styles.text}>{text}</Text>
</View>
)}
</Pressable>
</Modal>
</>
);
}
const styles = StyleSheet.create({
backdrop: { flex: 1 },
bubble: {
position: 'absolute',
backgroundColor: '#1f1f1f',
borderRadius: 10,
paddingVertical: 10,
paddingHorizontal: 12,
// Both platforms: Android reads elevation, iOS reads shadow*.
elevation: 6,
shadowColor: '#000',
shadowOpacity: 0.25,
shadowRadius: 10,
shadowOffset: { width: 0, height: 4 },
},
text: { color: '#fff', fontSize: 14, lineHeight: 20 },
});Two things this version deliberately skips. It always opens below the anchor; if your hint sits near the bottom of the screen, compare rect.y to the window height and set bottom instead of top. And it has no arrow. An arrow is a rotated 10-point square positioned at the anchor center, and in practice most apps skip it on mobile because the bubble is close enough to its control to read.
On the New Architecture, measureInWindow still works and is still the right call for this job. If you are already on Reanimated, the measure() worklet gives you the same rect synchronously on the UI thread, which only matters if the tooltip has to follow a moving anchor.
The two traps that eat tooltips
Android clips every child to its parent. On iOS a view can overflow its parent and still be visible. On Android overflow: 'visible' is only partly honoured and a bubble that pokes out of a card, a row, or a header gets cut at the edge. This is the reason every serious tooltip library renders into a Modal. Do not fight it with zIndex; the parent boundary wins.
zIndex does not cross siblings with elevation. Inside a ScrollView, a tooltip rendered by item three will be drawn under item four if item four has an elevation or comes later in the tree. Again the fix is to leave the tree: Modal, or a Portal host mounted at the app root such as @gorhom/portal, which is what you want when a Modal’s own window is a problem, for example when the tooltip has to appear above a bottom sheet that is itself a Modal.
One Modal-specific gotcha: a React Native Modal is a separate native window, so a tooltip inside it will sit above your toast host and your bottom sheet. Usually that is what you want. When it is not, Portal is the answer.
When to skip the tooltip entirely
If a control needs a tooltip to be understood, the first fix to try is a better label. A line of helper text under a form field is read by everyone, costs no gesture, and needs no measuring. Tooltips earn their place for dense screens where helper text on every control would double the height: a settings page, a stats dashboard, a pricing table.
This is also the default in apps generated with ShipNative: inline helper text first, an info-icon tooltip built on the Modal pattern above only where the screen is too dense for it. If you describe a screen that needs one, say which control and what the hint should say, and you get the Pressable, the measured rect, and the accessibilityHint together.