Coach mark

Free · MITReact Native · Expo

An onboarding spotlight: the screen dims around a cut-out over the measured target, and Next morphs the hole to the next target, changing shape and size in flight, while the tooltip glides alongside and a ring pulses on arrival.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add https://goodui.dev/native/r/coach-mark.json

Then run npx expo install --fix so react-native-reanimated match your Expo SDK. Files land in components/goodui-native/coach-mark.

Usage

import { useRef, useState, type ComponentRef } from "react";
import { View } from "react-native";
import { CoachMark } from "@/components/goodui-native/coach-mark/coach-mark";
import { IconButton } from "@/components/goodui-native/icon-button/icon-button";
import { SearchBar } from "@/components/goodui-native/search-bar/search-bar";
import { ComposeIcon } from "./icons"; // your own icon

export function InboxScreen() {
  const searchRef = useRef<ComponentRef<typeof View>>(null);
  const composeRef = useRef<ComponentRef<typeof View>>(null);
  const [tour, setTour] = useState(true);
  return (
    <View style={{ flex: 1, padding: 16, gap: 16 }}>
      <View ref={searchRef} collapsable={false}>
        <SearchBar placeholder="Search messages" />
      </View>
      {/* message list */}
      <View ref={composeRef} collapsable={false} style={{ position: "absolute", right: 16, bottom: 32 }}>
        <IconButton icon={(c, s) => <ComposeIcon color={c} size={s} />} accessibilityLabel="New message" variant="solid" tone="accent" />
      </View>
      <CoachMark
        open={tour}
        onOpenChange={setTour}
        steps={[
          { target: searchRef, title: "Find any message", description: "Search names, words and attachments." },
          { target: composeRef, shape: "circle", title: "Start a conversation", description: "Write to one person or a group." },
        ]}
      />
    </View>
  );
}

When to use

Use it for
  • A short first-run tour of three to five controls on a busy screen: filters, the compose button, a tab.
  • Pointing at one new feature after an update (a single step).
Reach for something else
  • Explaining the product before sign-up: use page-dots-onboarding.
  • A hint that appears on demand next to one control: use popover.
  • Anything the person must read to use the screen: put it in the screen itself.

Examples

Attach a target

<View ref={filtersRef} collapsable={false}>
  <ChipGroup options={filters} value={value} onValueChange={setValue} />
</View>

Tap anywhere to continue, localized

<CoachMark
  open={open}
  onOpenChange={setOpen}
  steps={steps}
  backdropPress="next"
  labels={{ next: "Weiter", back: "Zurück", skip: "Überspringen", done: "Fertig", stepOf: (s, t) => `${s} von ${t}` }}
/>

API reference

PropTypeDefault
steps*
The tour, in order.
CoachMarkStep[]—
open*
Whether the tour is showing.
boolean—
onOpenChange*
Called with `false` when the tour closes (finish, skip, Escape, Android back).
(open: boolean) => void—
step
Controlled step index. Pair with `onStepChange`.
number—
defaultStep
Uncontrolled first step. Default 0.
number0
onStepChange
Called with the new step index on Next and Back.
(step: number) => void—
onFinish
Called when Done is pressed on the last step.
() => void—
onSkip
Called with the step index when the tour is skipped or dismissed early.
(step: number) => void—
labels
Overrides for the built-in strings (buttons, step counter); a GooduiLocaleProvider pack sets them app-wide.
Partial<CoachMarkLabels>—
backdropPress
What a tap on the dimmed area does. Default `none`.
"next" | "close" | "none""none"
haptic
Button and step haptics. Default `true`.
booleantrue
testID
Identifier for end-to-end tests.
string—

Motion and haptics

  • Open: the dim fades in and the hole starts 44 pt wider than the target and closes in on it (a focus pull) on the morph spring.
  • Next and Back: the hole's position, size and corner radius spring to the next target on the morph spring, so a wide card hole becomes a round button hole in flight; the tooltip glides on the smooth spring, its arrow tracks the target and flips sides with a fade; the step text crossfades.
  • A 2 pt accent ring pulses out of the hole once per arrival.
  • Reduced motion: the hole and tooltip move at once and the content crossfades; no focus pull, no ring.
  • Haptic: select on every step change, tap on Skip, success on Done. haptic={false} turns them off.
  • The dim is one view with a very wide border whose inner edge is the hole, so the cut-out is plain layout animated on the UI thread: no svg masks, no snapshots.
  • Targets are measured once per step and when the window size changes.

Accessibility

  • The tooltip is modal (accessibilityViewIsModal; role dialog, aria-modal and aria-labelledby the step title on the web).
  • Each step announces 'N of M', its title and description.
  • Skip, Back and Next/Done are goodui Buttons. Escape on the web and the Android back button skip the tour.
  • The dim blocks touches to the app; with backdropPress set it is a button labelled after what it does (Next, Done or Skip tour).

Notes for AI

  • Each step's target is a ref to a View with collapsable={false} that is on screen; scroll it into view before opening or before that step.
  • shape='circle' for icon buttons, FABs and avatars; padding sets the gap around the element.
  • Control open; the component calls onOpenChange(false) on Done, Skip, Escape and back. onFinish runs on Done, onSkip(step) on an early exit.
  • Keep tours to five steps or fewer and copy to one sentence each.