Skip to content
Native Animation
Breathing Ring animation preview

Breathing Ring

A guided breathing screen with a fluid color field, a phase-aware center glow, and a progress point that travels around a timed ring.

$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-ring

About this animation

A guided breathing screen with a fluid color field, a phase-aware center glow, and a progress point that travels around a timed ring.

How it works

  1. The component measures its own container with onLayout; every visual anchor and scalable size is derived from that width and height.
  2. One cycleTime shared value advances in a useFrameCallback worklet and folds at the combined inhale, hold, and exhale duration. Breath fullness, the orbital point, phase boundaries, waves, and glow all derive from that clock.
  3. A second ambient clock moves four blurred Skia circles along slow elliptical paths. Both clocks read the same eased speed factor, so pausing gently slows the complete scene without moving the cycle position.
  4. A Reanimated reaction detects phase boundaries on the UI thread. Only those three changes per cycle cross to JavaScript to update the accessible label and call onPhaseChange.
  5. The three ring marks are calculated from the configured phase durations, so the constant-speed point reaches each mark exactly when its phase changes.

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 BreathingRing from './BreathingRing';

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

Customize the timing, copy, palette, callbacks, and imperative controls:

import { useRef } from 'react';
import BreathingRing, { type BreathingRingRef } from './BreathingRing';

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

  return (
    <BreathingRing
      ref={breathingRef}
      inhaleSeconds={5}
      holdSeconds={2}
      exhaleSeconds={6}
      palette="lagoon"
      inhaleLabel="Breathe in"
      holdLabel="Hold"
      exhaleLabel="Breathe out"
      onPhaseChange={(phase, cycle) => trackPhase(phase, cycle)}
      onPausedChange={(paused) => trackPause(paused)}
      onClose={() => navigation.goBack()}
    />
  );
}

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

Props

PropTypeDefaultDescription
inhaleSecondsnumber4Seconds spent inhaling.
holdSecondsnumber2Seconds spent holding.
exhaleSecondsnumber4Seconds spent exhaling.
palette'dusk' | 'lagoon' | BreathingPalette'dusk'Built-in preset or custom base, blob, glow, and button colors.
inhaleLabelstring'Breathe in'Label shown while inhaling.
holdLabelstring'Hold'Label shown while holding.
exhaleLabelstring'Breathe out'Label shown while exhaling.
initiallyPausedbooleanfalseStarts paused.
initiallyFavoritebooleanfalseStarts favorited.
initialSoundOnbooleantrueStarts with sound enabled.
fontFamilystringSystem fontFont family for the phase label.
onPhaseChange(phase, cycle) => void—Called when a phase begins, with its zero-based cycle number.
onPausedChange(paused: boolean) => void—Called when the paused state changes.
onFavoriteChange(favorite: boolean) => void—Called when the favorite state changes.
onSoundChange(on: boolean) => void—Called when the sound state changes.
onClose() => void—Handles close; the close button is hidden when omitted.
closeAccessibilityLabelstring'Close'Close button label.
pauseAccessibilityLabelstring'Pause' / 'Resume'Pause button label.
favoriteAccessibilityLabelstring'Favorite'Favorite button label.
soundAccessibilityLabelstring'Sound'Sound button label.
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: verified
  • Works in Expo Go because Skia, Reanimated, and Worklets are included in the SDK

Accessibility

Every control is a labeled button and toggle state is exposed to assistive technology. The active phase is a polite live-region text update, while inactive overlapping labels are hidden from the accessibility tree. With reduced motion enabled, the progress point and phase timing remain informative, but ambient blobs stop, breathing waves disappear, and the glow uses a steady opacity.