Skip to main content
← Blog

Guide Mode Animation — All I Wanted Was a Simple Slide

VauDium ·

Adding a slide animation to GuideBar turned into a battle with SafeArea, keyboards, and mount/unmount timing.

Guide Mode Animation — All I Wanted Was a Simple Slide

Fecit has a guide mode. When creating a task, it walks you through each step — “Current → Expectation → Obstacle → Tactics” — one at a time. A blue GuideBar appears at the top, replaces the navigation bar, guides you through the steps, and disappears when done.

The problem was that the GuideBar just popped in and popped out.

“It would look nice if it slid down from the top.”

Simple request. Not a simple implementation.

Step 1: Appearing Was Easy

const slideAnim = useRef(new Animated.Value(-50)).current;
useEffect(() => {
    Animated.spring(slideAnim, {
        toValue: 0, useNativeDriver: true, tension: 80, friction: 12
    }).start();
}, []);

A spring animation that bounces in from above. Worked perfectly. This took 5 minutes.

Step 2: Disappearing Was Hell

Problem 1 — The Component Dies First

GuideBar was rendered with guideMode ? <GuideBar /> : <RawTopBar />. Tapping X calls setGuideMode(false) → GuideBar unmounts. Animation? No time to run it.

“Just call onClose after the animation finishes, right?”

const handleClose = () => {
    Animated.timing(slideAnim, {
        toValue: -50, duration: 200, useNativeDriver: true
    }).start(() => onClose());
};

Correct in theory. But…

Problem 2 — One Frame of Flash

After the animation completes: onClose()setGuideMode(false) → GuideBar unmounts + SafeArea topColor changes. When this happens in the same frame, the screen flickers for one instant.

Tried requestAnimationFrame to delay by one frame. Still visible.

Problem 3 — SafeArea Stays Behind

We included the SafeArea region in GuideBar so the blue color extends to the very top. But when sliding out, the GuideBar moves up while the SafeArea’s topColor — rendered as a separate View — stays in place. A blue ghost lingers at the top.

Problem 4 — -50 Wasn’t Enough

translateY: -50 was less than the SafeArea height (~59px), so GuideBar didn’t fully disappear — it stopped right inside the SafeArea region.

const slideOutDistance = -(insets.top + 50);

Had to slide by SafeArea height + GuideBar height.

Problem 5 — Spring Bounces

Using Animated.spring to slide out made it go up, come back down slightly, then go up again. Tried overshootClamping: true but it still felt like it was stuttering. Animated.timing was the right choice for exits.

The Fix: Change the Structure

We ended up changing the fundamental structure.

Before: guideMode ? <GuideBar> : <RawTopBar>

GuideBar and RawTopBar swap places. When GuideBar disappears, RawTopBar appears — but this swap happens in one frame and looks jarring.

After: RawTopBar always renders, GuideBar overlays with absolute positioning

{guideMode && (
    <View style={{position: "absolute", top: 0, left: 0, right: 0, zIndex: 10}}>
        <GuideBar ... />
    </View>
)}
<RawTopBar ... />

This means:

  • When GuideBar slides out, RawTopBar is already there behind it
  • No SafeArea color transition issues (GuideBar includes its own SafeArea padding)
  • No unmount timing issues (animation completes → onClosesetGuideMode(false))

Bonus: The Keyboard Problem

Guide mode auto-advances to the next step when the keyboard dismisses:

Keyboard.addListener("keyboardDidHide", () => {
    // advance to next step
});

In Retrospect mode, step 0 is the satisfaction picker (a modal, not a keyboard). But if the keyboard is open when starting guide mode, it dismisses → keyboardDidHide fires → skips satisfaction and jumps straight to retrospect.

The fix was simple:

const currentField = steps[guideStepRef.current]?.field;
if (currentField === "satisfaction") return;

Ignore keyboard dismissal during the satisfaction step.

Lessons Learned

  1. Appear animations are easy; disappear animations are hard. On mount, the component exists so it can animate. On unmount, the component needs to die but the animation isn’t done yet.

  2. Ternary swaps (A ? X : Y) are worse than overlays (absolute + always rendered) for transitions. Both components need to coexist for a smooth handoff.

  3. SafeArea is trickier than you think. If a component needs to cover the SafeArea region, include that padding inside the component itself rather than relying on a parent wrapper.

  4. Spring for entrances, timing for exits. A bouncy entrance feels lively. A bouncy exit feels broken.

In the end, “slide in, slide out” took about an hour. The code is barely 10 lines, but the journey to those 10 lines was long.