About this animation
A travel discovery flow linked by a moving voice orb, with configurable tours, labels, colors, and user avatar.
How it works
- One
stageshared value coordinates the home, listening, and results animation. An explicit active-screen state controls hit testing and accessibility, so transparent screens cannot intercept input. A single time value and normalized audio level drive the Skia shader without React renders per frame. startListening,showResults, andresetexpose the same transitions used by the demo autoplay.- A horizontal Gesture Handler pan moves the home tour carousel and springs to the nearest card. Gesture Handler pressables handle card, heart, and arrow taps independently; a drag cancels the press. Result rows use local assets supplied by the consumer.
- Region chips select a carousel card. Hearts toggle favorites per tour. Cards and arrows open tour details with a save toggle; the bottom bar opens Home, Favorites, Saved tours, and Profile. These collections last for the mounted session.
- The keyboard controls open an editable question; submit it with the center button or the keyboard Search key. The microphone starts a simulated listening session and finishes it into results. Supply
levelandonAskto connect a host audio service; this component does not request microphone permission. - The demo stops autoplay on pointer down or touch start, before the control action runs, and cancels all pending timers.
- Reduced motion keeps the orb still and uses short fades instead of the shared-orb flight.
Install
npx expo install @shopify/react-native-skia expo-image react-native-gesture-handler react-native-reanimated react-native-worklets @expo/vector-iconsUsage
import { useRef } from 'react';
import VoiceTourAssistant, { type VoiceTourAssistantRef } from './VoiceTourAssistant';
export function TravelSearch() {
const assistant = useRef<VoiceTourAssistantRef>(null);
return <VoiceTourAssistant ref={assistant} tours={tours} prompt="Find a quiet mountain escape" />;
}Props
| Prop | Type | Description |
|---|---|---|
tours | VoiceTour[] | Home carousel and results. Each item includes id, title, subtitle, description, image, and rating. |
regions | string[] | Selectable region chips. |
location | string | Current location label. |
prompt | string | Phrase to transcribe and show with results. |
userAvatar | ImageSourcePropType | Avatar shown in the submitted prompt bubble. |
labels | object | Overrides for ask, search, trip heading, and results copy, plus home, favorites, saved, profile, emptyCollection, keyboardPlaceholder, submitSearch, saveTour, removeSavedTour, and closeDetails. |
colors | object | Accent, background, and three shader colors. |
level | SharedValue<number> | Optional live normalized voice level from 0 to 1. |
onAsk | () => void | Called when listening starts. |
onTourPress | (tour: VoiceTour) => void | Called when a carousel card, arrow, collection item, or result opens tour details. |
onClose | () => void | Called when the flow is reset from a close control. |
style | StyleProp<ViewStyle> | Optional root style. |
testID | string | Root test identifier. |
The forwarded ref exposes advanceCarousel(), startListening(), showResults(), and reset().
Compatibility
- Expo SDK 58 and React Native 0.88
- Reanimated 4.7, Worklets 0.13, and Skia 2.14
- iOS and Android: supported
- Web: supported.
Accessibility
Interactive controls have labels and roles, region chips remain individually tappable, and system reduced-motion preference disables the orb flight and shader motion.
Why this one
Most voice search flows discard their identity between listening and results. Here, the chrome orb stays present through all three stages, making the answer feel like a continuation of the conversation.
Icons: Feather from @expo/vector-icons (Lucide-compatible stroke family, ISC).



