A Day Polishing Guide Mode — a Stuck Bar, One Black Frame, and a Single Question
A section bar wouldn't unstick, an animation went in and came back out, the keyboard had a white void behind it, and one question was too hard to answer. All in one day.
A Day Polishing Guide Mode — a Stuck Bar, One Black Frame, and a Single Question
Fecit has a guide mode: a conversational flow that walks you through one task, asking step by step what you’re trying to achieve, where you stand, and what might get in the way. Yesterday we added a focus scrim to it — a dark layer over the steps you haven’t reached yet. Today turned into the day of dealing with the aftermath. It started with a bug report, took a full loop through adding and removing animation, and ended with rewriting a single sentence.
1. The bar that wouldn’t unstick — onLayout is relative to the parent
The first report: “The Project section bar in the task detail stays sticky the whole time.”
The mobile task detail uses a two-tier sticky header: each section bar’s Y coordinate is measured with onLayout, and a bar pins to the top once you scroll past it. When we laid down the scrim yesterday, we wrapped each section bar in <View>{scrim}<SectionBar/></View> — and left the measuring onLayout on the inner bar.
React Native’s layout.y is relative to the direct parent. The moment the wrapper appeared, all five sections measured y=0. A defensive filter treats y=0 as “not measured yet” and drops it — but the Project section alone survived through an old special case (“it really is at the top, 0 is fine”). The result: every other sticky stop vanished, and the Project bar sat pinned through the entire scroll.
The fix was moving onLayout onto the wrapper — and deleting the now-stale special case. The reason for deleting it is the fun part: if the same regression ever happens again, it now fails as “sticky quietly doesn’t engage” instead of “a bar is glued to the screen forever.” When you wrap a measured element, the measurement has to move with it. It’s the kind of mistake that type-checks clean and looks fine that day.
2. Completion timing — the scrim doesn’t wait for the guide bar
Finish the guide and a completion message appears; the guide bar takes its bow and leaves 1.5 seconds later. The scrim used to wait out that same 1.5 seconds — which felt wrong in practice. You’re done, but the room is still dark.
So we reordered it: the scrim lifts the moment the completion message appears, and only the guide bar stays behind to say goodbye. “Done → the screen brightens” lands as one beat now. Desktop already worked this way; mobile caught up.
3. The animation that went in and came back out — one frame of black
Feeling ambitious, we added stagecraft: a 300ms fade-in of darkness on entry, a 400ms curtain-lift on completion. It built, it type-checked, and then on a real device, closing the guide flashed a single frame of black.
The diagnosis was the longest journey of the day. The repro was “close while on the title step” — but on mobile, nothing covers the title at all. Something that doesn’t exist can’t flash, so we chased ghosts for a while. The culprit was the scrim on the block just below the title, and the mechanism was useNativeDriver: opacity driven by the native driver can, at unmount, restore its style to the stale JS-side value for a frame as the animation node detaches. That frame drew a black overlay at the wrong opacity. Switching to the JS driver made other things feel off instead.
That settled the direction: clean beats cinematic. We stripped every fade and went back to instant-on, instant-off — and deleted desktop’s pre-existing 0.25s fade-out while we were at it. Animation is a business that goes bankrupt over a single ghost frame. And an instantaneous blackout has its own kind of decisiveness.
4. Desktop symmetry — the title is always a “passed step”
With mobile settled, desktop showed three mismatches.
The title was getting scrimmed. The guide’s rule is “passed steps stay bright” — your earlier answers are raw material for the next question. The title is step one, so from step two onward it is always a passed step. Desktop alone still had a title-only overlay dimming it. Deleted.
The scroll to the last step stopped short. The final step is the Tactics field at the very top. Mid-way through the smooth scroll upward, a focus() scheduled 0.3s later triggered the browser’s “reveal the focused element” auto-scroll, cancelling the animation in flight. One line — focus({preventScroll: true}) — lets the scroll finish its trip.
A thin dark band above the Tactics field. The content’s 12px top padding belongs to no block, so lifting the active block above the scrim left just that strip dark. The topmost block’s lift now extends to the very top of the body.
All three repairs come from the same principle: if mobile and desktop share a concept, they share a grammar.
5. The white thing behind the keyboard — a 34px double-count
On mobile, the area behind the rising keyboard was white. The identity of that white: KeyboardAvoidingView in padding mode creates a keyboard-height void with no content in it — no content, no scrim, just the page background showing through.
Extending the bottom scrim strip by the keyboard’s height should fix it, and the first attempt failed: it also dimmed the formatting accessory bar sitting on top of the keyboard. The cause was the arithmetic — insets.bottom + keyboardHeight. On iOS, the height that keyboardWillShow reports already includes the home-indicator inset. Adding the inset again pushed the strip 34px past the top of the keyboard, right into the accessory. Using the keyboard height alone while the keyboard is open fits exactly. Not a limitation of the approach — a double-count.
6. Rewriting one question — an unanchored question is the hardest question
The last item wasn’t code but a sentence. Of all the guide’s steps, “Current” — where you write down where things stand — was reportedly the hardest to answer.
The old question was “Where are you now?” (Korean: “현재 상황은 어떤가요?”). The more we looked at it, the clearer the problem: a question with no reference point has infinite scope, and a question that anything could answer is actually the hardest one to answer. The guide deliberately keeps your just-written “hope” bright on screen — and the question wasn’t using it.
We tried directive phrasing (“Write down the facts of where you stand”) and a specificity tag-along sentence, but every other step is a question — one imperative breaks the register — and the tail was clutter. The final version is one sentence:
“Where are you now, relative to your hope?”
It’s a distance question that turns your own answer, still visible above, into the measuring stick. Holding the desired future and the present side by side — that contrast is what turns plans into action, and there’s a solid body of motivation research behind it. Whether that structure fits inside a single sentence is what makes the difference.
What today taught us
One: measuring code and the measured element are one body — a single wrapper changes the coordinate system. Two: animation is only animation when it’s flawless — one ghost frame bankrupts 400ms of elegance. Three: a good question carries its own reference point — merely pointing at an answer already on screen shrinks the terror of the blank page.
Guide mode came out of today considerably sturdier. Next is watching where people actually stop inside this flow.