Skip to content
Native Animation
ambienttravelfocusparallaxinteractive

Window Seat Clouds

A flight-mode scene built around an airplane window: an endless cloud sea drifting past the glass, a sliding shade, and quiet controls for pause, ambient…

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

npx nativeanimation add window-seat-clouds

About this animation

A flight-mode scene built around an airplane window: an endless cloud sea drifting past the glass, a sliding shade, and quiet controls for pause, ambient sound, and the shade.

How it works

  1. Every window part is measured from the container via onLayout and sized off the glass width, so the whole assembly keeps its proportions in a modal, a card, or on a tablet.
  2. Inside the glass, three copies of the panorama sit in a row — normal, mirrored, normal — so every seam shows the same edge pixels. A single offset shared value advances in a useFrameCallback worklet and folds back every two panorama widths, which makes the drift endless and the wrap invisible.
  3. Pausing eases a speedFactor shared value between 1 and 0 over 700 ms; the drift, the bob, and the bank all read it, so the view decelerates without any jump in position.
  4. The bob (translateY) and the bank (rotate) ride on a glass-sized wrapper, keeping their pivot at the window center; the panorama is 18 % taller than the glass so the motion never exposes an edge.
  5. The shade is a panel inside the glass whose translateY follows one shade shared value: 6 % of the glass at rest (just the grip lip), the full glass when closed. The same value dims the light halo and drives the shade icon in the button.

Install

npx expo install expo-image expo-linear-gradient react-native-reanimated

Usage

import WindowSeatClouds from './WindowSeatClouds';

export function FlightScreen() {
  return <WindowSeatClouds skySource={require('./assets/cloud-sea.jpg')} />;
}

Use the knobs, callbacks, and imperative controls to adapt the pattern:

import { useRef } from 'react';
import WindowSeatClouds, {
  type WindowSeatCloudsRef,
} from './WindowSeatClouds';

export function FlightScreen() {
  const windowRef = useRef<WindowSeatCloudsRef>(null);

  return (
    <WindowSeatClouds
      ref={windowRef}
      skySource={require('./assets/cloud-sea.jpg')}
      horizonY={0.45}
      glassHorizonY={0.52}
      driftSpeed={0.03}
      turbulence={0.6}
      timeValue="40"
      timeUnit="min"
      distanceValue="205"
      distanceUnit="mi"
      accentColor="#C98B5A"
      fontFamily="YourSansFont"
      numberFontFamily="YourMonoFont"
      onPausedChange={(paused) => trackPause(paused)}
      onShadeChange={(closed) => trackShade(closed)}
      onSoundChange={(on) => trackSound(on)}
    />
  );
}

The forwarded ref exposes pause(), resume(), togglePause(), closeShade(), openShade(), and toggleShade().

The panorama should be a landscape image around 1600×914; horizonY is measured on that image, and the strip renders it at its own aspect ratio with contentFit="cover".

Props

PropTypeDefaultDescription
skySourceImageSourcePropTypeRequiredLandscape cloud panorama shown through the window.
horizonYnumber0.45Height (0–1) of the horizon inside skySource.
glassHorizonYnumber0.52Where the horizon sits inside the glass (0 = top, 1 = bottom).
driftSpeednumber0.04Drift speed, in glass widths per second.
turbulencenumber1Gentle bob and bank of the view, 0 disables it.
initiallyPausedbooleanfalseStarts paused.
initiallyShadeClosedbooleanfalseStarts with the shade down.
initialSoundOnbooleantrueAmbient-sound toggle initial state.
timeLabelstring'Time remaining'Label above the time value.
timeValuestring'40'Time value shown in the bottom-left corner.
timeUnitstring'min'Unit shown after the time value.
distanceLabelstring'Distance remaining'Label above the distance value.
distanceValuestring'205'Distance value shown in the bottom-right corner.
distanceUnitstring'mi'Unit shown after the distance value.
accentColorstring'#C98B5A'Hex accent of the active sound button.
fontFamilystringSystem fontFont family for labels and units.
numberFontFamilystringSystem fontFont family for the time and distance values.
onPausedChange(paused: boolean) => void—Called with the new paused state.
onShadeChange(closed: boolean) => void—Called with the new shade state; true means closed.
onSoundChange(on: boolean) => void—Called with the new ambient-sound state.
pauseAccessibilityLabelstring'Pause drift' / 'Resume drift'Label of the pause button.
soundAccessibilityLabelstring'Ambient sound'Label of the sound button.
shadeAccessibilityLabelstring'Window shade'Label of the shade button.
styleStyleProp<ViewStyle>—Outer-container style.
testIDstring—Test identifier for the outer container.

Compatibility

  • Expo SDK 57
  • React Native 0.86
  • Reanimated 4.5
  • iOS: supported, not yet verified on a device
  • Android: supported, not yet verified on a device
  • Web: verified
  • Works in Expo Go because all native dependencies are included in the SDK

Accessibility

The three controls are labeled buttons; the pause button announces its state, and the sound and shade buttons report theirs as checked. Every label can be overridden. When reduced motion is enabled, the drift and the turbulence stop (the panorama sits still) and the shade switches with a 200 ms opacity fade instead of sliding.