About this animation
An airplane seat picker with radial fills and a reclining upgrade preview, with configurable seats, pricing, accent color, and fonts.
Why this one
The airplane has a nose, cockpit, windows, exit doors, and wings rather than a generic grid. Selecting a seat reveals its color through an actual circular Skia clip; an upgrade zooms toward the selected row and demonstrates the additional room with a reclining seat drawn in profile. The previous choice remains selected until the upgrade is accepted.
How it works
onLayoutsizes a 90%-width fuselage with four 2–2 Comfort+ rows and twenty-four 3–3 economy rows. Visible seats measure 44 and 32 points respectively, with 44-point touch targets.- A composed pan/tap gesture drives a continuous UI-thread scroll position. Wings move at 92% of the cabin's scroll speed. Buttons offer equivalent navigation without dragging.
- A single selection reveal value drives the new seat's clipped circular fill and the previous seat's clearing. Touch pressure uses a separate spring so release cannot cancel selection.
- Premium selection opens a dismissible preview, dims other seats, and derives row zoom from preview progress. Skia rotates the seat back from 0 to 30 degrees in a slow loop.
- Accepting the upgrade commits the seat and adds a soft purple halo. Prices use a shared numeric value, digit carry, and distance fades; digits settle on whole numbers.
- React updates only for layout and selection/preview/confirmation events. The demo controls a cancellable 7-second timeline through the public ref.
Install
npx expo install @shopify/react-native-skia expo-blur expo-linear-gradient react-native-gesture-handler react-native-reanimated react-native-workletsWrap your app in GestureHandlerRootView. Follow the installed Skia version's web setup and load CanvasKit before importing this module on web. In this catalog, the host must register this demo through its existing withSkia loader. Fonts are optional; the component uses system fonts unless supplied. The demo's Geist fonts are loaded by the catalog host.
Usage
import SeatMapPicker from './seat-map-picker';
export function SeatScreen() {
return <SeatMapPicker onConfirm={(seat) => saveSeat(seat.id)} />;
}With inventory and personality knobs:
import { useRef } from 'react';
import { SeatMapPicker, type SeatMapPickerRef } from './seat-map-picker';
export function FlightSeats() {
const picker = useRef<SeatMapPickerRef>(null);
return (
<SeatMapPicker
ref={picker}
flightLabel="Your upcoming flight"
seats={[{ id: '12B', row: 12, letter: 'B', occupied: true }]}
accentColor="#0EA5E9"
premiumColor="#C084FC"
seatPrice={24}
upgradePrice={89}
legroomInches={5}
zoomScale={1.6}
springDamping={28}
colorScheme="light"
onConfirm={(seat) => saveSeat(seat.id)}
/>
);
}saveSeat represents your application's persistence callback. Give the component a bounded parent; its root uses flex: 1.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
seats | SeatMapSeat[] | [] | Occupancy by ID in the fixed 28-row layout; omitted IDs are available. |
brand | ReactNode | — | Optional leading brand tile and wordmark. |
flightLabel | string | Your flight | Flight metadata. |
title | string | Choose your seat | Main heading. |
accentColor | string | #0EA5E9 | Economy selection fill. |
premiumColor | string | #C084FC | Premium selection, seat preview, and halo. |
seatPrice | number | 24 | Economy seat fee in dollars. |
upgradePrice | number | 89 | Premium seat fee in dollars. |
legroomInches | number | 5 | Additional legroom shown in the preview. |
zoomScale | number | 1.6 | Preview zoom, clamped to 1–2. |
springDamping | number | 28 | Spring damping, with a minimum of 28. |
fontFamily | string | System | Body and metadata typeface. |
mediumFontFamily | string | fontFamily | Heading and button typeface. |
monoFontFamily | string | System | Price and row-number typeface. |
colorScheme | 'light' | 'dark' | 'auto' | auto | Appearance. |
onSeatChange | (seat: SeatSelection) => void | — | Selection committed. |
onConfirm | (seat: SeatSelection) => void | — | Confirmation pressed. |
onPreview | (seat: SeatSelection) => void | — | Premium preview opened. |
onInteraction | () => void | — | User touches the map or a control. |
accessibilityLabel | string | Airplane seat map | Map label. |
style | StyleProp<ViewStyle> | — | Root styling. |
testID | string | — | Root test identifier. |
SeatSelection includes id, location, premium, and price. Comfort+ rows use A–D; economy rows use A–F.
Ref methods
selectSeat(id): select economy or open a premium preview; occupied and unknown IDs are ignored.scrollToRow(row): scroll to a row, clamped to 1–28.upgrade(): commit the previewed seat.dismissPreview(): preserve the previous selection and close the preview.reset(): clear the selection and return to the nose.
Demo timeline
After an 80 ms start delay: scroll to row 14 at 120 ms, select 14A at 650 ms, return to the nose at 1,100 ms, preview 3C at 1,730 ms, accept the upgrade at 4,050 ms, and restart at 7,000 ms. The preview opens before 3 seconds. Any user touch clears all remaining timers and stops the loop until playback is re-enabled or the demo remounts.
Compatibility
Designed for Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, worklets 0.13.0, and Skia 2.14.0. Uses installed Expo Go modules; a custom native host must configure these dependencies. Android blur uses a BlurTargetView with the SDK 31+ blur method and a translucent fallback on older Android versions.
Supports iOS, Android, and web. The production catalog must load Skia lazily; this folder does not modify the catalog registry.
Accessibility
All seats expose a button role, row/position/class/price label, occupied state, selection state, and activation action. Occupied seats cannot be selected. Upgrade opening, selection, dismissal, and confirmation announce state changes; selection details also use a polite live region. All buttons and seat hit regions are at least 44 points. The preview identifies itself as modal to assistive technology.
Reduced motion removes zoom, wing parallax, reclining motion, press movement, radial expansion, and glass blur. Selection/preview changes fade over 200 ms; prices switch directly without rolling. Scroll jumps directly to its destination. Up/down buttons are the tap alternative to dragging.
Icons: Lucide (ISC), copied from the repository kit. The Northwind tile and gradient follow the kit's demo brand definition. No image assets or remote requests are needed.
Layout
At 320×694 and 390×844, controls sit inside the bottom sheet, the upper-right viewer close area stays free, and the preview begins around 1.8 seconds.



