Skip to main content

Recipes

Remembering what was seen

The library keeps no record of it. It shows the tour, and whether the tour should show at all is your call — so no storage engine gets forced into your bundle.

autoStart is the gate, and it starts the tour the moment it turns true, which means an async read costs you nothing.

import { MMKV } from 'react-native-mmkv';

const mmkv = new MMKV();

export const Scanner = () => {
const [seen, setSeen] = useState<boolean | null>(null);

useEffect(() => {
setSeen(mmkv.getBoolean('tour.scanner') ?? false);
}, []);

return (
<Cicerone.Provider
steps={STEPS}
autoStart={seen === false}
onStop={() => mmkv.set('tour.scanner', true)}
>
{/* targets */}
</Cicerone.Provider>
);
};

While seen is null the read has not landed yet and autoStart stays false, so the tour never flashes before you know the answer. AsyncStorage works the same way.

A replay button

const { start } = useCicerone();

start() runs the tour whenever you call it. Clearing your own flag, if you keep one, is up to you.

Letting the user tap the highlighted element

By default one layer covers the screen and any tap advances the tour, which is what most onboarding wants. When the point of the step is for the user to actually press the thing:

<Cicerone.Provider steps={STEPS} allowTargetInteraction>

Now four strips surround the hole instead of one full screen layer, so the target stays live.

Use overlayPress="none" if taps should do nothing at all and the card buttons are the only way forward.

Tracking the tour

<Cicerone.Provider
steps={STEPS}
onStart={() => analytics.track('tour_started')}
onStepChange={(index, step) => analytics.track('tour_step', { index, id: step.id })}
onStop={(reason) => analytics.track('tour_ended', { reason })}
/>

onStop tells you which of the three happened: finished, skipped, or manual if you called stop() yourself. It fires once, when the tour ends, not when the closing animation finishes.

Forcing which side the card goes

The card picks whichever side has room. When you know better:

{ id: 'header-action', title: '...', text: '...', placement: 'bottom' }

Putting a blur behind the hole

renderBackdrop draws inside the cut out, under the scrim. The library ships no blur, so bring the one your app already uses.

import { BlurView } from 'expo-blur';

<Cicerone.Provider
steps={STEPS}
renderBackdrop={() => <BlurView intensity={20} style={StyleSheet.absoluteFill} />}
/>;