About this animation
A library search control that expands into an input and highlights results, with configurable books, results, accent color, and fonts.
Why this one
Typical expanding search bars stop at the control. This one carries the transition through a credible reading product: two curated shelves and a reading-goal card recede, recent searches and useful suggestions arrive in a 35 ms cascade, typed results crossfade in, and each matching phrase receives a left-to-right accent underline. The input remains a real TextInput, and the same continuous progress drives the field shape, title exit, background focus, Cancel action, search icon, and discovery panel.
How it works
onLayoutderives the closed trigger position, expanded field width, and responsive book-cover sizing from the component itself.- One continuous
openProgressshared value springs from 0 to 1 and drives the field width and position, title lift, shelf dimming, Cancel reveal, and staggered discovery content. - A real
TextInputowns normal React text state while result and highlight transitions stay on the UI thread. - Result titles are split around the live query; a rounded underline at 30% of the line height scales beneath the matching letters without covering them.
expo-blurtargets the library layer on Android SDK 31+ and falls back to its translucent surface treatment elsewhere.- The forwarded ref lets demos and product flows open, close, focus, clear, or type programmatically without adding demo-only props.
Install
npx expo install expo-blur expo-linear-gradient @shopify/react-native-skia react-native-reanimated react-native-workletsIcons are from Lucide (ISC) and are included as local paths in icons.ts. Skia web projects need CanvasKit initialized before this component is loaded.
Usage
import ExpandingSearch from './ExpandingSearch';
export function ReadingLibrary() {
return <ExpandingSearch />;
}Customize the content, palette, and imperative flow:
import { useRef } from 'react';
import ExpandingSearch, { type ExpandingSearchRef } from './ExpandingSearch';
export function ReadingLibrary() {
const searchRef = useRef<ExpandingSearchRef>(null);
return (
<ExpandingSearch
ref={searchRef}
brandName="Lumen"
accentColor="#E58A45"
completedBooks={18}
targetBooks={24}
recentSearches={['Poetry', 'Design history', 'Ocean essays']}
onQueryChange={(query) => updateSearchAnalytics(query)}
onItemPress={(book) => openBook(book)}
/>
);
}The ref exposes open(focus?), close(), setQuery(query), clear(), and focus().
Props
| Prop | Type | Default | Description |
|---|---|---|---|
brandName | string | 'Lumen' | Product name shown above the library title. |
title | string | 'Library' | Screen title. |
firstShelfLabel | string | 'Continue reading' | Label for the first shelf. |
secondShelfLabel | string | 'Saved for later' | Label for the second shelf. |
readingGoalLabel | string | 'Reading goal' | Label for the annual reading progress card. |
completedBooks | number | 18 | Books completed toward the reading goal. |
targetBooks | number | 24 | Target number of books for the reading goal. |
books | ExpandingSearchBook[] | DEFAULT_BOOKS | Six books used to build the two shelves. |
recentSearches | string[] | DEFAULT_RECENTS | Recent search chip labels. |
suggestions | ExpandingSearchItem[] | DEFAULT_SUGGESTIONS | Suggested books shown before typing. |
results | ExpandingSearchItem[] | DEFAULT_RESULTS | Books shown after the user types. |
placeholder | string | 'Search your library' | Input placeholder. |
backgroundColor | string | '#FAF7F2' | Warm screen background. |
textColor | string | '#211D18' | Primary ink color. |
mutedColor | string | '#7E756B' | Muted ink color. |
accentColor | string | '#E58A45' | Accent used by focus and result highlights. |
fontFamily | string | undefined | Interface font family. |
mediumFontFamily | string | undefined | Medium interface font family. |
monoFontFamily | string | undefined | Monospaced font family used for changing figures. |
serifFontFamily | string | undefined | Serif font family used for titles and book covers. |
onOpenChange | (open: boolean) => void | undefined | Called when the open state changes. |
onQueryChange | (query: string) => void | undefined | Called when the query changes. |
onRecentPress | (query: string) => void | undefined | Called when a recent search is selected. |
onItemPress | (item: ExpandingSearchItem) => void | undefined | Called when a suggestion or result is selected. |
accessibilityLabel | string | 'Open library search' | Screen-reader label for the search trigger. |
style | StyleProp<ViewStyle> | undefined | Optional style for the outer container. |
testID | string | undefined | Test identifier applied to the outer container. |
Compatibility
- Expo SDK 58
- React Native 0.88.0
- Reanimated 4.7.1
- React Native Skia 2.14.0
- iOS and Android: supported
- Web: supported
- Expo Go: supported by the SDK 58 bundled modules; use a development build after adding native dependencies to another Expo project
Accessibility
The trigger, Cancel action, recent chips, suggestions, and results expose button roles and descriptive labels. The input is a native TextInput. Reduced Motion keeps the content crossfade and immediate state clarity while removing field travel, title lift, stagger movement, and press scaling.
Customization notes
Movement follows gestures, scroll position, or interruptible spring physics. A global duration is not exposed; reduced-motion fades retain their short fixed timing.




