Bug Report: The Calendly Widget Flash That Took 4 Hours to Fix
Title
Calendly iframe widget flashes for 100-500ms after booking confirmation before showing success screen
Description
When users complete a booking through our embedded Calendly widget, there's a jarring visual flash where the booking widget remains visible for a fraction of a second before transitioning to our custom confirmation screen. This happens on every single booking, creating a poor user experience at the most critical moment of our conversion funnel.
The flash duration varies between 100-500ms depending on browser performance, but it's always noticeable enough to feel broken. Users see the Calendly widget briefly "hang" after they click confirm, then it suddenly disappears and our confirmation appears. Not exactly the smooth, professional experience we're going for.
Steps to Reproduce
- Navigate to
/book?topicId=any_valid_id - Select a topic and click "Continue to Scheduling"
- In the Calendly widget, select any available time slot
- Fill in your name, email, and phone number
- Click "Schedule Event" to confirm the booking
- OBSERVE: The Calendly widget remains visible for approximately 100-500ms
- Then the widget disappears and the confirmation card appears
Expected Result
The Calendly widget should disappear instantly (within 1-2ms) when the user confirms their booking, followed by a smooth fade-in animation of the confirmation card. The transition should feel seamless and professional.
Actual Result
The Calendly widget remains fully visible for 100-500ms after the user clicks confirm. This creates a "frozen" appearance where the widget appears to hang, then suddenly vanishes and is replaced by the confirmation card. The flash is jarring and makes the app feel buggy.
Environment
| Component | Version/Details |
|---|---|
| Framework | Next.js 15.3.3 with Turbopack |
| UI Library | Radix UI (shadcn/ui) + Framer Motion |
| Router | Next.js App Router (client-side) |
| React Version | Latest (bundled with Next.js 15) |
| Browser | Chrome (latest), also reproduced in Firefox and Safari |
| Dev Server | localhost:9002 |
| Third-party Widget | Calendly embedded iframe (PostMessage API) |
Root Cause Analysis
Here's what was happening under the hood:
That 98ms gap between "user clicked confirm" and "widget disappears" is what users saw as the flash. And here's the kicker: everything we tried initially just moved the problem around instead of solving it.
The Journey: 7 Attempts to Fix One Flash
Let me take you through our debugging journey. Each attempt taught us something valuable about React, iframes, and the importance of thinking outside the framework.
❌ Attempt 1: Framer Motion's AnimatePresence
The Idea: Use Framer Motion's exit animations to smoothly transition the widget out.
Why it failed: The mode="wait" actually made things worse! It kept the widget visible during the exit animation, extending the flash duration instead of eliminating it. Lesson learned: exit animations are great for smooth transitions, terrible for hiding things instantly.
❌ Attempt 2: Good Old setTimeout
The Idea: Add a delay before showing the confirmation, maybe the timing would work out?
Why it failed: This just made the widget stay visible longer! We were literally adding delay to an already delayed process. Sometimes the obvious solution is obviously wrong.
❌ Attempt 3: Inline CSS Display None
The Idea: Use inline styles to hide the widget immediately with CSS.
Why it failed: The inline style still depends on React state! The bookingConfirmed variable needs to update (async), then React needs to re-render (also async), then the style applies. Same timing problem, different syntax.
❌ Attempt 4: Direct DOM Manipulation (So Close!)
The Idea: Forget React, just grab the DOM element and hide it with vanilla JavaScript.
Why it failed: This was actually really close! The problem? This code ran in the parent component's callback, which is called AFTER the event handler processes the message. By the time this ran, the widget had already been visible for too long. The approach was right, but the location was wrong.
❌ Attempt 5: CSS Opacity Transitions
The Idea: Fade the widget out smoothly with CSS transitions before removing it.
Why it failed: The widget still flashed before the opacity transition could start. CSS transitions are smooth, but they're not instant. And they still need React to apply the transitioning state first.
❌ Attempt 6: Absolute Positioning Overlay
The Idea: Keep both elements in the DOM and just overlay the confirmation on top.
Why it failed: Even the overlay needs React to render it! So the widget was still visible while React was processing the state change to show the overlay. We were trying to hide something by putting something else on top of it, but that something else also took time to appear.
✅ Attempt 7: Synchronous DOM Manipulation in Event Handler
The Idea: Hide the widget immediately when the PostMessage arrives, before any async operations.
Why it worked: By hiding the widget synchronously in the event handler (before any callbacks or state updates), we achieved instant hiding in about 1ms. The widget disappears before React even knows anything happened. Then React can take its sweet time updating state and rendering the confirmation card with beautiful animations.
✅ The Solution That Actually Worked
The fix was embarrassingly simple once we found it: one line of synchronous JavaScript in the right place.
Location: src/components/calendly-widget.tsx, line 306
The magic line: containerRef.current.style.display = 'none';
Execution time: ~1ms (down from 100-500ms)
User impact: Zero visible flash, seamless transition
The New Timeline
Key Learnings
This bug taught us several valuable lessons:
- React isn't always the answer. For timing-critical UI updates, synchronous DOM manipulation beats React state every time.
- Location matters more than implementation. We tried DOM manipulation in Attempt 4, but in the wrong place. Moving it to the event handler made all the difference.
- Async operations have a cost. React's state updates are async for good reasons (batching, performance), but that 50-100ms delay is very real and very visible for certain UI operations.
- Third-party iframes are tricky. You can't control their lifecycle, so you have to work around them. Hiding is faster than unmounting.
- Simple solutions often win. After trying complex animation libraries, overlays, and timing tricks, one line of vanilla JavaScript solved everything.
Impact & Metrics
| Metric | Before Fix | After Fix | Improvement |
|---|---|---|---|
| Widget hide time | 100-500ms | ~1ms | 99.5% faster |
| User-perceived delay | Very noticeable | Imperceptible | 100% improvement |
| Code complexity | High (animations, delays) | Low (1 line) | Significantly simpler |
| Booking completion rate | TBD | TBD | Monitoring... |
Attachments & Evidence
Console logs showing the timing:
- Before: Multiple "Prefill data changed, reinitializing widget..." logs after booking
- After: Single "Widget hidden immediately (synchronous DOM operation)" log
Screen recordings available showing:
- The flash occurring with previous implementation
- Smooth transition with new implementation
Code references:
src/components/calendly-widget.tsx(lines 305-308)src/app/book/page.tsx(confirmation card implementation)
Conclusion
Sometimes the best debugging sessions are the ones that humble you. We spent hours trying increasingly complex React-based solutions when the answer was a single line of vanilla JavaScript in the right place. The Calendly widget flash is gone, our users get a smooth booking experience, and we learned a valuable lesson about when to step outside the framework and use the platform directly.
Remember: React is an amazing tool, but it's not the only tool. When milliseconds matter, sometimes you need to go back to basics.
This bug report documents a real issue encountered in production and the journey to fix it. Total time invested: ~4 hours. Lines of code changed for the fix: 1. Lessons learned: Priceless.






























