Skip to content
Native Animation
Breathing Hills animation preview

Breathing Hills

A full-screen guided-breathing landscape where three soft hill layers rise and fall like a slow earthen tide.

$9 for this animation

Checkout opens soon. Leave your email and we will tell you the moment you can buy it.

One email when checkout opens. No spam.

Add to your project

Copy the component and its assets into your app with the Native Animation CLI, or paste the agent prompt into Claude Code, Codex or Cursor once the MCP server is connected.

npx nativeanimation add breathing-hills

About this animation

A full-screen guided-breathing landscape where three soft hill layers rise and fall like a slow earthen tide.

How it works

  1. The component measures its own container and derives every position and size from its width and height.
  2. One monotonic cycleTime shared value advances in a Reanimated frame callback. Breath fullness, phase boundaries, cycle count, and completion all derive from that clock.
  3. Each hill is a closed Skia path with three cubic Bézier crests. The rear, middle, and front layers evaluate breath fullness with progressively longer delays to create parallax.
  4. A Skia gradient blends its horizon color toward a brighter inhaled color, while the orb follows the same eased breath value.
  5. A UI-thread reaction detects phase changes and only crosses to JavaScript when the accessible instruction or cycle counter must update.
  6. Palette changes interpolate every sky, landscape, orb, and text color over one second.

Install

npx expo install @shopify/react-native-skia react-native-reanimated react-native-worklets

On web, CanvasKit must finish loading before this component mounts. Call LoadSkiaWeb in your web entry point, then render the application:

import { LoadSkiaWeb } from '@shopify/react-native-skia/lib/module/web';

LoadSkiaWeb().then(() => {
  root.render(<App />);
});

Usage

import BreathingHills from './BreathingHills';

export function BreathingScreen() {
  return <BreathingHills />;
}

Customize timing, labels, palette, cycle count, callbacks, and imperative controls:

import { useRef } from 'react';
import BreathingHills, { type BreathingHillsRef } from './BreathingHills';

export function BreathingScreen() {
  const breathingRef = useRef<BreathingHillsRef>(null);

  return (
    <BreathingHills
      ref={breathingRef}
      inhaleSeconds={5}
      holdSeconds={2}
      exhaleSeconds={7}
      cycles={5}
      palette="night"
      onPhaseChange={(phase, cycle) => trackPhase(phase, cycle)}
      onComplete={() => navigation.goBack()}
    />
  );
}

The forwarded ref exposes pause(), resume(), and restart().

Props

PropTypeDefaultDescription
inhaleSecondsnumber4Seconds spent inhaling.
holdSecondsnumber2Seconds spent holding.
exhaleSecondsnumber6Seconds spent exhaling.
palette'dusk' | 'sage' | 'night' | HillsPalette'dusk'Built-in preset or custom sky, hill, orb, and text colors.
inhaleLabelstring'Breathe in'Instruction shown while inhaling.
holdLabelstring'Hold'Instruction shown while holding.
exhaleLabelstring'Breathe out'Instruction shown while exhaling.
cyclesnumber0Number of cycles to run; zero repeats forever.
fontFamilystringSystem fontFont family for instructions and the cycle counter.
onPhaseChange(phase, cycle) => void—Called when a phase begins, with a zero-based cycle number.
onComplete() => void—Called after a finite session finishes.
styleStyleProp<ViewStyle>—Outer-container style.
testIDstring—Test identifier for the outer container.

Compatibility

  • Expo SDK 57
  • React Native 0.86
  • Reanimated 4.5
  • React Native Worklets 0.10
  • React Native Skia 2.6
  • iOS: supported, not yet verified on a device
  • Android: supported, not yet verified on a device
  • Web: supported, not yet visually verified
  • Works in Expo Go because Skia, Reanimated, and Worklets are included in the SDK

Accessibility

The current breathing instruction is exposed as a polite live-region update, and hidden phase labels are removed from the accessibility tree. With reduced motion enabled, all hills follow the same breath value without parallax or horizontal crest movement; the breathing timing and phase announcements remain intact.