About this animation
A pet welcome screen with swinging ribbons that open into details, with configurable pets, copy, background color, and fonts.
Why this one
Instead of a static onboarding illustration, the composition responds like a set of hanging curtains. Pull an individual ribbon, sweep across the group, then let the selected ribbon become the next screen. The original image stays mounted throughout the transition.
How it works
- The host is measured with
onLayout; ribbon widths divide its width evenly and lengths use the hero height. - Each ribbon enters 80 ms after its neighbor with an explicitly configured, nearly critical spring and a finite, damped four-degree swing.
- A Gesture Handler tap/pan race supplies immediate pressure feedback, elastic vertical pull, and horizontal curtain motion. Release uses gesture velocity.
- One shared detail progress controls ribbon geometry, portrait position, and welcome/detail opacity. Independent shared values handle pressure and local physics.
- The selected ribbon expands into the detail background.
replay()brings the welcome back; all gestures can interrupt ongoing springs. - The demo performs entrance → Biscuit pull → horizontal sweep → continue → return. Physical interaction cancels its timers.
Install
npx expo install expo-image react-native-gesture-handler react-native-reanimated react-native-workletsCopy this folder and place your screen inside a GestureHandlerRootView. Load any custom font families in your app before rendering. The component has no catalog or theme imports; the demo uses the catalog's fonts. No icons or logos are used.
Usage
import { HangingBannersWelcome } from './hanging-banners-welcome';
<HangingBannersWelcome
pets={[
{ name: 'Luna', color: '#D9DEFF', image: require('./kitten.webp'), length: 0.65 },
{ name: 'Biscuit', color: '#CFF5D6', image: require('./puppy.webp'), length: 0.83 },
]}
title="Find your new best friend nearby"
body="Meet the friends waiting for a home."
cta="Meet them"
onContinue={(pet) => openProfile(pet)}
/>const welcome = useRef<HangingBannersWelcomeRef>(null);
<HangingBannersWelcome
ref={welcome}
pets={pets}
swingAngle={3}
staggerMs={80}
backgroundColor="#17192D"
textColor="#F9F7F2"
mutedColor="#C7C5D4"
accentColor="#FF7A5C"
buttonTextColor="#1E2250"
fontFamily="YourRegularFont"
mediumFontFamily="YourMediumFont"
titleFontFamily="YourBoldFont"
/>
// welcome.current?.pull(1); welcome.current?.replay();Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
pets | HangingPet[] | Required | One to four names, colors, image sources, and relative lengths; extra entries are ignored. |
title | string | Find your new best friend nearby | Welcome heading. |
body | string | A little hello could be the start of something wonderful. | Supporting copy. |
cta | string | Meet them | Continue button and label. |
brand | string | None | Optional text-only wordmark below the ribbons. |
detailBody | string | Your next chapter starts with a new best friend. | Detail copy and announcement. |
backLabel | string | Back to friends | Return button. |
backgroundColor | string | #FBF6F1 | Welcome surface. |
textColor | string | #1E2250 | Headings, names, detail ink, and return border. |
mutedColor | string | #646477 | Welcome body and wordmark. |
accentColor | string | #FF7A5C | Continue surface and glow. |
buttonTextColor | string | #1E2250 | Continue ink. |
fontFamily / mediumFontFamily / titleFontFamily | string | System | Body, controls, and heading fonts. |
swingAngle | number | 4 | Swing amplitude, capped at four degrees. |
staggerMs | number | 80 | Entrance stagger in milliseconds. |
onSelect / onContinue | (pet, index) => void | None | Discrete JS callbacks. |
onInteraction | () => void | None | Stops host-driven playback on touch. |
style | StyleProp<ViewStyle> | None | Host layout overrides. |
testID | string | hanging-banners-welcome | Root prefix, with -pet-N, -continue, and -back controls. |
HangingPet.accessibilityLabel overrides its spoken name. length is clamped to 0.4–0.85 of the hero area. Ref methods: pull(index), replay(), sway(), and continue().
Compatibility
Targets Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, and Worklets 0.13.0. Uses Expo Go's installed modules. No Skia or native custom module is required. Supports iOS, Android, and web.
The root fills its parent. The host screen supplies safe-area insets; the component adds none. Copy stays in a scrollable flow for compact containers, landscape, and larger accessibility fonts. Portraits cap at 256 pt, preserving 3× density for the provided 768 px images. The top-right area contains no text or added controls. For tablet use, a centered phone-width host keeps the welcome composition compact. The full palette is caller-controlled; choose pet backgrounds that contrast with your detail ink.
Accessibility
Pet banners expose button roles, caller-overridable labels, and an activate action. Tapping provides an alternative to dragging. Continue and return buttons are 56 pt high; banner widths are at least 75 pt in the standard 300 pt host. Selection and detail changes announce their content. Hidden welcome/detail regions are removed from the accessibility tree. Font scaling remains enabled.
Reduced motion removes falls, pulls, rotations, and curtain sweeps. Welcome/detail changes use 200 ms fades; pressure feedback stays subtle.




