Skip to main content

Chapter 12: The Shake Interaction

The hero moment. Shake the phone, feel the haptics, see a random memory appear.

This is the feature that makes GratitudeTree special. The user shakes their phone, feels a heavy haptic thud, and a random past journal entry animates into view — like reaching into a physical jar and pulling out a memory. It combines motion detection, haptics, random selection, and animation into one delightful interaction.


What You'll Build

  • A shake detection system using the Capacitor Motion plugin
  • A shake gesture algorithm that filters out noise
  • A full-screen overlay that shows a random entry with animation
  • Heavy haptic feedback on shake detection
  • A "Shake Again" action and dismiss gesture
  • A web fallback button for development and non-native contexts

What You'll Learn

  • The DeviceMotion API and accelerometer data
  • How to detect a shake gesture from raw motion data
  • CSS keyframe animations for the reveal effect
  • The useCallback and useRef hooks for event handling
  • Cooldown patterns to prevent rapid re-triggers

Step 1: Install the Motion Plugin

npm install @capacitor/motion
npx cap sync

The Motion plugin provides access to the device's accelerometer — the sensor that detects movement and orientation.

tip

If you get any terminal errors during this step, paste the full error output directly into Claude Code. It can read error messages and fix them faster than you can search Stack Overflow.


Step 2: Understand Shake Detection

A shake is a rapid back-and-forth motion. The accelerometer reports acceleration on three axes (x, y, z). We detect a shake by looking for acceleration that exceeds a threshold:

Normal movement:   x: 0.1, y: 0.2, z: 9.8  (gravity alone)
Shake movement: x: 15.3, y: -12.7, z: 22.1 (much larger values)

The algorithm:

  1. Read accelerometer values
  2. Calculate total acceleration: √(x² + y² + z²)
  3. Subtract gravity (~9.8): anything above a threshold is a shake
  4. Require multiple threshold-exceeding samples within a time window
  5. Trigger the shake event with a cooldown to prevent rapid re-triggers

Step 3: Build the Shake Detection Hook

>_Claude Code — Step 3
Send this to Claude Code:
> Create a custom React hook at src/hooks/useShakeDetection.ts that uses Capacitor's Motion plugin to detect phone shakes. It should accept an onShake callback, calculate acceleration from the device motion data, distinguish real shakes from normal movement, and enforce a cooldown between triggers. Only listen on native platforms.
Why: This hook bridges the accelerometer hardware to React state. It filters noise from real shakes using a threshold and cooldown.
What to expect: Claude Code will create src/hooks/useShakeDetection.ts with Motion listener setup, acceleration calculation, threshold check, and cooldown enforcement.
Tip: If you get any terminal errors during this step, paste the full error output directly into Claude Code. It can read error messages and fix them faster than you can search Stack Overflow.
$ claude "Create a custom React hook at src/hooks/useShakeDetection.ts that uses Capacitor..."

Key design decisions

Threshold of 30: Normal phone movement (walking, gesturing) produces accelerations of 10-15. A deliberate shake hits 25-40+. We use 30 to avoid false positives while being responsive enough to feel natural.

Cooldown of 1000ms: Without a cooldown, a single shake gesture (back-forth-back) would trigger multiple events. The cooldown ensures one shake = one reveal, and gives the animation time to complete.

useRef for timing: We use useRef instead of useState for lastShakeTime because we don't want re-renders when the timestamp updates. This is a performance pattern — refs are mutable without triggering renders.

useCallback for stability: The motion listener is registered once in useEffect. If handleMotion changed on every render, we'd need to tear down and recreate the listener. useCallback keeps it stable.


Step 4: Build the Shake Reveal Overlay

When a shake is detected, we show a full-screen card with a random entry.

>_Claude Code — Step 4
Send this to Claude Code:
> Create a ShakeReveal component at src/components/ShakeReveal.tsx that shows a full-screen overlay when a journal entry is revealed. It should have a dark backdrop with a fade-in, and the card should bounce in with a playful animation. Show a 'From Your Jar' header, display the entry content (text or photo), the date, and a 'Shake Again' button. Dismiss on backdrop click or Escape key. Also add the CSS keyframe animations to index.css.
Why: This is the hero UI — the magical reveal when a user shakes their phone to discover a past memory.
What to expect: Claude Code will create ShakeReveal.tsx with animated overlay, entry display, dismiss/shake-again actions, and CSS keyframe animations.
Tip: If you get any terminal errors during this step, paste the full error output directly into Claude Code. It can read error messages and fix them faster than you can search Stack Overflow.
$ claude "Create a ShakeReveal component at src/components/ShakeReveal.tsx that shows a fu..."

The bounce-in animation uses a spring-like cubic bezier curve. The card starts small, overshoots slightly, bounces back, and settles. It feels playful and rewarding — exactly the vibe we want for a "jar reveal" moment.


Step 5: Wire Shake into App.tsx

Now connect the shake detection to the reveal overlay.

>_Claude Code — Step 5
Send this to Claude Code:
> Wire up the shake-to-discover feature in App.tsx. When a shake is detected, pick a random entry, fire a heavy haptic, and show the ShakeReveal overlay. Add a 'shake again' action that picks a different random entry. Dismiss the overlay when the user taps the backdrop or X button.
Why: Wiring the shake detection hook, random entry selection, haptics, and the reveal overlay together in App.tsx.
What to expect: Claude Code will update App.tsx with shakeEntry state, handleShake/handleShakeAgain callbacks, useShakeDetection integration, and ShakeReveal rendering.
Tip: If you get any terminal errors during this step, paste the full error output directly into Claude Code. It can read error messages and fix them faster than you can search Stack Overflow.
$ claude "Wire up the shake-to-discover feature in App.tsx. When a shake is detected, pick..."

Step 6: Add a Web Fallback Button

Shaking doesn't work in the browser or Simulator. We need a tappable fallback for testing.

>_Claude Code — Step 6
Send this to Claude Code:
> Add a shake fallback button to the Header for web and Simulator testing. Show a sparkles icon that triggers the same shake reveal when tapped. Only show it when there are entries to reveal. Update App.tsx to pass the shake handler to the Header.
Why: Shaking doesn't work in the browser or Simulator, so we need a tappable fallback for testing.
What to expect: Claude Code will update Header.tsx with optional onShake/hasEntries props and a Sparkles button, and update App.tsx to pass the props.
Tip: If you get any terminal errors during this step, paste the full error output directly into Claude Code. It can read error messages and fix them faster than you can search Stack Overflow.
$ claude "Add a shake fallback button to the Header for web and Simulator testing. Show a ..."

Now there's a sparkles icon in the header. Tapping it triggers the same reveal as shaking — useful for testing and for web users who can't shake their browser.

>_Claude Code
Try asking:
> I built a shake detection feature for my Capacitor iOS app. It works on real devices but I need a way to test it in the browser and Simulator. Is there a better approach for testing motion-based features?
Tip: You can also simulate shake events in Xcode's Simulator via the Device menu → Shake Gesture. But the Capacitor Motion plugin doesn't always respond to the simulated shake.
$ claude "I built a shake detection feature for my Capacitor iOS app. It works on real dev..."

Step 7: Test the Shake Feature

On the web

  1. Start the dev server: npm run dev
  2. Create a few entries (you need at least one)
  3. Tap the sparkles icon in the header
  4. A card should animate in with a random entry
  5. Tap Shake Again — a different random entry should appear
  6. Tap the dark overlay or the X button to dismiss

On a physical device

  1. Build and deploy to a real iPhone (we'll cover this in Chapter 13)
  2. Physically shake the phone
  3. Feel the heavy haptic thud
  4. See the random entry card bounce in
  5. Shake again while the card is dismissed
The Simulator Limitation

The iOS Simulator can simulate a shake via Hardware → Shake Gesture (or Cmd + Ctrl + Z), but the Capacitor Motion plugin may not respond to simulated shakes. For reliable testing, use a physical device or the header button.


Step 8: Commit Your Progress

git add .
git commit -m "Add shake-to-discover: motion detection, random entry reveal, bounce animation"
git push

⚠️Common Issues

Checkpoint

Checkpoint — End of Chapter 12

Your app should now:

  • Shaking the phone triggers a random entry reveal (physical device)
  • The sparkles button in the header works as a web/Simulator fallback
  • The reveal card has a bounce-in animation that feels playful
  • Heavy haptic feedback fires on shake detection
  • "Shake Again" picks a different random entry
  • Tapping the overlay or X button dismisses the reveal
  • The shake has a cooldown to prevent rapid re-triggers

What's Next

The hero feature is built. In the next chapter, we handle the iOS-specific details — safe area insets for the notch, keyboard behavior, app icons, and splash screens. These are the details that make your app feel truly native.

𝕏

Follow @parvsondhi for build threads, tips, and updates on this tutorial.

Next: Chapter 13 — iOS-Specific Work →