About this animation
A language picker with a character-by-character text preview and configurable languages, title, search placeholder, and fonts.
Why this one
Flags describe countries rather than languages. Native names, English subtitles, and custom monograms make this selector easier to recognize without making assumptions about nationality. A real localized greeting shows exactly what a choice changes, on a warm paper surface rather than an isolated list.
How it works
- Selection stores the previous and next language together in React state. Each character starts its shared-value progress in
useLayoutEffect, after the new text commits. - Characters at matching positions stay still. Changed characters briefly cycle through two deterministic destination-alphabet glyphs, 40 ms each, then settle from left to right. Heading staggering is 25 ms; longer supporting lines compress their staggering to finish within 600 ms.
- Hidden character measurements provide intrinsic widths on all platforms. Slot widths animate continuously; deleted characters shrink away and added characters enter from scale 0.8. A comma separates greeting and name onto two editorial lines.
- Each row has separate values for press feedback, selection, and filtering. The selected tint sweeps inside the row surface in 200 ms; its radio fills with a spring. Filtered rows collapse, moving subsequent rows with an interruptible spring.
- Reduced motion uses a 200 ms crossfade of complete text lines with no scramble, displacement, or pop. Filtering uses opacity and height timing.
- The demo selects English, Spanish, and French at 0, 650, and 1650 ms, followed by Japanese at 3000 ms. The cycle repeats every 4.8 seconds.
Install
npx expo install react-native-reanimated react-native-worklets @shopify/react-native-skiaConfigure Reanimated and Skia for the host app. On web, load CanvasKit before importing this component, using the host's Skia loader. The demo also uses expo-linear-gradient. Icons: Lucide (ISC); the copied paths are in icons.ts.
Usage
import { LanguagePickerMorph, type LanguageOption } from './language-picker-globe';
const languages: LanguageOption[] = [
{
id: 'en-US', nativeName: 'English', englishName: 'English',
monogram: 'EN', color: '#6B8064', greeting: 'Hello, Alex',
subtitle: 'Everything is ready.', alphabet: 'abcdefghijklmnopqrstuvwxyz',
},
{
id: 'es-ES', nativeName: 'Español', englishName: 'Spanish',
monogram: 'ES', color: '#AE7653', greeting: 'Hola, Alex',
subtitle: 'Todo está listo.', alphabet: 'abcdefghijklmnopqrstuvwxyzáéíóúñ',
},
];
<LanguagePickerMorph languages={languages} />;const picker = useRef<LanguagePickerMorphRef>(null);
<LanguagePickerMorph
ref={picker}
languages={languages}
initialLanguage="es-ES"
colorScheme="light"
morphDuration={500}
fontFamily="YourInterfaceFont"
headingFontFamily="YourEditorialFont"
monoFontFamily="YourMonoFont"
onLanguageChange={language => saveLanguage(language.id)}
style={{ flex: 1 }}
/>;
// From an event handler:
picker.current?.select('en-US');Load custom fonts in the host before showing the component. Use short greetings and subtitles sized for the host container. The component has no imports from the catalog or shared theme.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
languages | readonly LanguageOption[] | Required | Names, monograms, accents, localized copy, scramble alphabets, and optional system-font fallback. |
initialLanguage | string | First language | Initial locale. |
onLanguageChange | (language: LanguageOption) => void | — | Selection callback on the React thread. |
fontFamily | string | System | Interface and subtitle font. |
headingFontFamily | string | System | Editorial greeting font. |
monoFontFamily | string | System | Tile and metadata font. |
searchPlaceholder | string | Search languages | Search label and placeholder. |
title | string | App language | List heading. |
previewLabel | string | A little preview | Paper eyebrow. |
colorScheme | 'light' | 'dark' | 'auto' | auto | Theme selection. |
morphDuration | number | 600 | Duration, clamped to 200–600 ms. |
style | StyleProp<ViewStyle> | — | Root container override. |
testID | string | — | Test identifier. |
The ref exposes select(id) and reset(). Unknown locale IDs are ignored. reset() clears search and selects the initial locale. An empty language list is supported. Locale IDs should be unique BCP 47 identifiers; use six-digit hex accents and nonempty alphabets.
Compatibility
Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, and Skia 2.14.0. These modules are available in the matching Expo Go build. Supports iOS, Android, and web. No additional native dependency is required.
The component measures its own container with onLayout. The demo fills 89% of the screen width, reserves the top-right viewer close area, and uses a scrollable list at 320×694 and 390×844. The greeting size scales from approximately 38 to 47.2 pt in those containers, capped at 47.2 pt. The eight languages remain reachable without squeezing their touch targets.
Accessibility
Rows have radio roles and selected state, with both native and English names in their labels. The preview has a single localized accessible label and accessibilityLanguage; animated character layers are hidden from screen readers. Search has a label, and empty results are announced. Rows have at least 52 pt touch targets. Reduced motion removes scrambling and spring feedback; text crossfades in 200 ms. Scrolling is the standard platform interaction and every selection is available by touch.





