About this animation
A coffee tasting wheel with a recommendation reveal and configurable product image, recommendation copy, accent color, and fonts.
Why this one
Replace a text questionnaire with a barista’s tasting wheel. A six-axis radar makes your palate visible as you choose. The same wheel becomes a seal on the recommended bag, connecting discovery to the product rather than displaying an unrelated result panel.
How it works
- An
onLayoutmeasurement sizes a single Skia canvas with six inner families and eighteen outer notes.TextPathfollows each segment’s arc. - A pan measures angular movement around the center. Tangential velocity drives
withDecay; touching again cancels inertia. A Gesture Handler tap selects a segment independently of pan recognition. - Selection drives shared arrays for wedge extrusion, color saturation, and the spring-driven radar. React updates only on discrete selections.
- A small dot travels from the selected segment to its chip. Selecting the third note aligns the circular mean of the notes with the top.
- One shared transition shrinks the actual wheel into a product seal and reveals the bag. Two carry-aware digit columns count to the matching percentage.
- The demo calls the public ref at 650, 1200, and 1780 ms. The reveal finishes by 2530 ms; the cycle restarts at 5100 ms. Any touch stops playback.
Install
Copy this folder into your app, excluding demo/.
npx expo install @shopify/react-native-skia expo-image react-native-gesture-handler react-native-reanimated react-native-workletsTested dependency versions: Skia 2.14.0, expo-image ~58.0.17, Gesture Handler ~3.3.0, Reanimated 4.7.1, worklets 0.13.0. Wrap your application in GestureHandlerRootView. For web, configure CanvasKit loading in the host app and mount this component after Skia has loaded. Pass a bundled font through labelFont for curved text on web. Native falls back to a system font.
Usage
import FlavorWheelMatch from './flavor-wheel-match';
<FlavorWheelMatch
labelFont={require('./assets/Interface-Regular.ttf')}
imageSource={require('./assets/coffee.jpg')}
onAddToBag={() => addCoffeeToCart()}
/>const wheel = useRef<FlavorWheelMatchRef>(null);
<FlavorWheelMatch
ref={wheel}
labelFont={require('./assets/Interface-Regular.ttf')}
roastName="Mountain Roast"
description="Sweet, rounded, and bright."
actionLabel="Add to bag — $18"
accentColor="#B45309"
extrusion={10}
colorScheme="light"
matchPercent={92}
onSelectionChange={setFlavorNotes}
onMatch={showRecommendation}
/>;
wheel.current?.select(6);
wheel.current?.rotate(Math.PI / 3);
wheel.current?.reset();Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
| imageSource | ImageSource | — | Local or host-supplied bag artwork |
| roastName | string | Your roast | Result title |
| description | string | A balanced, comforting cup. | Tasting copy |
| actionLabel | string | Add to bag | Purchase action |
| matchPercent | number | 92 | Match score, clamped to 0–99 |
| fontFamily | string | system | Interface font |
| titleFontFamily | string | system | Editorial result font |
| monoFontFamily | string | system | Counter font |
| labelFont | DataSourceParam | native system | Curved label font; required on web |
| accentColor | string | #B45309 | Accent; six-digit hex color |
| colorScheme | light / dark / auto | auto | Surface and text theme |
| extrusion | number | 10 | Selected wedge offset in points |
| onSelectionChange | (notes: string[]) => void | — | Selection event |
| onMatch | (notes: string[]) => void | — | Third-note event |
| onAddToBag | () => void | — | Purchase event |
| onInteraction | () => void | — | Touch event for playback cancellation |
| style | StyleProp<ViewStyle> | — | Root overrides |
| testID | string | — | Test identifier |
duration | number | 580 | Match highlight reveal duration in milliseconds. Spring physics and reduced-motion timing are unchanged. |
imageSource | ImageSource | undefined | Bag artwork supplied by the host application. |
roastName | string | 'Your roast' | Recommendation name. |
description | string | 'A balanced, comforting cup.' | Recommendation description. |
actionLabel | string | 'Add to bag' | Purchase button label. |
matchPercent | number | 92 | Final matching percentage. |
fontFamily | string | undefined | Interface font, with the system font as fallback. |
titleFontFamily | string | undefined | Editorial headline font. |
monoFontFamily | string | undefined | Numeric font. |
labelFont | DataSourceParam | undefined | Bundled font source for curved Skia labels; required on web. |
accentColor | string | '#B45309' | Warm accent. |
colorScheme | 'light' | 'dark' | 'auto' | 'auto' | Theme selection. |
extrusion | number | 10 | Selected wedge extrusion in points. |
onSelectionChange | (notes: string[]) => void | undefined | Called once per selection change. |
onMatch | (notes: string[]) => void | undefined | Called when the third note creates a recommendation. |
onAddToBag | () => void | undefined | Purchase callback. |
onInteraction | () => void | undefined | User interaction callback, used to stop demo playback. |
style | StyleProp<ViewStyle> | undefined | Container overrides. |
testID | string | undefined | Test identifier. |
Ref: select(index), rotate(radians), reset(). FLAVOR_NOTES exports the ordered notes. The fourth selection is ignored until a note is removed. The score and coffee are supplied by the host; this is an interaction component, not a recommendation engine.
Compatibility
Expo SDK 58 / React Native 0.88.0. Compatible with the installed Expo Go stack. The demo includes local artwork and uses the catalog’s loaded fonts; the reusable component has no catalog imports.
Accessibility
Every action has a button role and label. The horizontally scrolling note buttons provide at least 44-point targets for all eighteen notes, including segments too narrow to be accessible touch targets. “Turn wheel” offers a tap alternative to dragging; chips remove selected notes. Important selection and match changes are announced, and selection/result text uses a polite live region. Reduced motion removes dot flights, inertia, rotation alignment, extrusion, and radar springs. The wheel fades out and the result fades in over 200 ms; the match score changes immediately.
Icons: Lucide (ISC). The Ember & Oat demo mark comes from the repository’s brand kit.




