About this animation
A destination carousel with layered photos and masked headlines, with configurable destinations, fonts, accent color, and depth intensity.
Why this one
The photograph, headline, and foreground move at different speeds. Unlike a static text cutout, the lettering rises inside individual clipping boxes while trees and towers stay in front. Each destination places its headline in its own sky region, with an optional height budget and horizontal anchor to preserve readability. Fully opaque white lettering and a soft 24 pt black shadow keep the headline readable over bright skies. The demo pairs three local scenes with the Northwind travel kit.
How it works
onLayoutmeasures the container; all scene framing uses that size.- A continuous UI-thread position drives the looping horizontal carousel, zoom, parallax, and detail opacity.
- Render the photograph first, clipped headline letters second, and its matching transparent foreground third. Both image layers use identical bounds,
cover, and centered positioning. - Letters enter after 80 ms, staggered by 35 ms, with a 400 ms rise of 40% of their height. Outgoing letters leave upward. The foreground stays above the letters.
- Compose pan and tap with Gesture Handler. Drag release projects velocity and settles with a nearly critical spring. A separate shared value handles button pressure.
- The demo advances every 2.4 seconds and alternates gentle vertical depth every 1.2 seconds. Touch stops autoplay. Reduced motion uses a 200 ms dissolve with no zoom or parallax.
Install
npx expo install expo-image expo-linear-gradient react-native-gesture-handler react-native-reanimated react-native-worklets @shopify/react-native-skiaCopy this folder without demo/. Place it inside your app's GestureHandlerRootView. Use SDK-compatible versions; exact versions are recorded in piece.json. For Skia on web, load CanvasKit before mounting the component, using your app's existing lazy Skia loader. No new fonts are installed. Icons: Lucide (ISC); copied paths live in icons.ts.
Usage
import { DepthHeadlineCarousel } from './depth-headline-carousel';
const destinations = [{
id: 'retreat',
image: require('./retreat.jpg'),
foreground: require('./retreat-front.png'),
headline: ['QUIET', 'DAYS'],
name: 'Forest Retreat',
details: ['4 nights', '3 excursions', 'Moderate'] as const,
guide: 'EK',
}];
<DepthHeadlineCarousel destinations={destinations} onBook={openBooking} />;const carousel = useRef<DepthHeadlineCarouselRef>(null);
<DepthHeadlineCarousel
ref={carousel}
destinations={destinations}
depthIntensity={0.6}
transitionDuration={650}
accentColor="#70D3EF"
headlineFontFamily="YourMediumFont"
onIndexChange={updateDestination}
/>;
// From an event handler or timer:
carousel.current?.next();Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
| destinations | readonly DepthHeadlineDestination[] | required | Paired photos, headline lines, name, three details, guide initials, optional connector, headlineTop, headlineHeight, and headlineLeft |
| initialIndex | number | 0 | Starting page |
| brand | ReactNode | none | Upper-left brand content |
| fontFamily | string | system | Body font |
| headlineFontFamily | string | system | Headline and label font |
| serifFontFamily | string | system | Small editorial connector font |
| accentColor | string | #70D3EF | Page count and guide accent |
| depthIntensity | number | 1 | Depth amplitude, clamped to 0–2 |
| transitionDuration | number | 600 | Minimum letter entrance timeline; extended for longer headlines |
| bookLabel | string | Book adventure | Booking action label |
| onIndexChange | (index: number) => void | none | Active-page change |
| onBook | (destination) => void | none | Booking action |
| onInteractionStart | () => void | none | Stop an external autoplay timer |
| accessibilityLabel | string | Adventure destinations | Carousel label |
| style | StyleProp<ViewStyle> | none | Container overrides |
| testID | string | none | Testing identifier |
Ref methods: next(), previous(), goTo(index), and setDepth(amount) (−1 to 1). Keep autoplay outside the component. Supply one or two short uppercase headline lines. headlineTop defaults to 0.12 and headlineLeft to 0.08. Set headlineHeight to a fraction of container height to fit both lines inside a scene's transparent sky; omitting it retains the original headline sizing. Foreground PNGs must retain the photo's full original dimensions; never crop away transparent margins.
Compatibility
Targets Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, worklets 0.13.0, and Skia 2.14.0. Uses modules bundled with this project's Expo Go version. The catalog registration and CanvasKit loader are the host application's responsibility.
Accessibility
The carousel exposes increment/decrement accessibility actions and announces destination changes. Tap the left portion to go back; tap the rest of the scene to advance. The booking action has its own button role and activation action. All touch targets are at least 44 pt. Reduced motion removes all letter travel, depth, zoom, and spring overshoot, retaining a 200 ms crossfade. The demo announces its saved state through a polite live region.
Implementation notes
- The cabin headline uses the central sky at x=0.32 W and y=0.09–0.23 H. The lighthouse headline uses x=0.22 W and y=0.08–0.22 H, above the cutout's retained lower sky. Both use a 0.14 H height budget. The castle keeps its original position and sizing. Connectors sit beside the first word.
- Offline glyph/foreground-alpha measurements at 390×844 and 320×694 estimate visible letter area: cabin LOST 100%, WOODS 96–97%; castle CASTLES 80–82%; lighthouse EDGE 95%, MAP 80–82%. White is 100% opaque; the text shadow is black at 25% with radius 24 and no outline.
- Brand content stays in the upper left. No control or important copy occupies the viewer's upper-right close-button area.
- Destination details have a separate bottom gradient, three equal metric columns, a 44 pt CTA, and a guide avatar. At both target sizes the controls have space between the navigation row, details, and booking row.
- The initial stagger finishes within the first second; autoplay changes destination at 2.4 seconds. Photos and foreground PNGs match at 768×1344 and use identical fit, position, and bounds.
- Reveals use individual letter clipping boxes. No solid block passes over text. The deliberate photo veils are background gradients.




