About this animation
A rating sheet with a continuously changing face and configurable rating labels, feedback reasons, surface color, and fonts.
Why this one
Five fixed emoji reactions turn feedback into five disconnected states. This face shares one continuous value with the slider: its mouth, eyes, blush, and color evolve under your finger. The expressions progress from a deep frown through a slight frown, a neutral mouth, and a gentle smile to an open smile with joyful curved eyes. No angled eyebrows compete with the expression. Submitting brings a small personal reaction and a drawn confirmation check. Reasons change with the rating, so the feedback stays relevant.
How it works
- Measure the container with
onLayout; scale the face and slider from its width. - Drive the radial gradient and custom facial paths from a single continuous Reanimated
ratingshared value. Skia updates on the UI thread. - Compose pan and tap using
Gesture.Exclusive. Drag anywhere along the track, then spring to the nearest integer with the release velocity. - Notify React only when the rounded rating changes. Reveal the label after its text commits, and stagger reason chips by 40 ms.
- Blink on a cleaned-up 3–5-second timer. Submit once, bounce or nod, draw the Lucide check, and dismiss after 1,200 ms.
- Push the background back with scale, corner rounding, and opacity. The handle supports downward dragging and tap-to-dismiss.
Install
npx expo install @shopify/react-native-skia expo-linear-gradient react-native-gesture-handler react-native-reanimated react-native-workletsPlace the application inside GestureHandlerRootView. Load any custom fonts before rendering. For web, initialize CanvasKit with Skia's LoadSkiaWeb before importing the component; an existing Skia-enabled Expo application can use its current loader. Icons: Lucide (ISC). The copied paths in icons.ts come from the project's approved kit.
Usage
import { RatingSheetFace } from './rating-sheet-emoji-free';
export function Feedback() {
return (
<RatingSheetFace
title="How was your order?"
subtitle="Tell the team what stood out."
onSubmit={(rating, reasons) => saveFeedback({ rating, reasons })}
>
<DeliveredOrder />
</RatingSheetFace>
);
}Customize its personality and control it through a ref:
const feedback = useRef<RatingSheetFaceRef>(null);
<RatingSheetFace
ref={feedback}
initiallyOpen={false}
initialRating={3}
springDamping={28}
autoCloseMs={1600}
colorScheme="light"
surfaceColor="#FFF8F1"
fontFamily="Geist_400Regular"
mediumFontFamily="Geist_500Medium"
titleFontFamily="InstrumentSerif_400Regular"
negativeReasons={['Temperature', 'Wait time']}
positiveReasons={['Fast', 'Friendly', 'Great taste']}
/>
// From an event handler:
feedback.current?.open();
feedback.current?.setRating(5);Import RatingSheetFaceRef from the same folder. Keep the reason list to two or three short strings for narrow phones. Supply five rating labels when localizing. The component imports no theme or catalog code; the demo passes its own fonts and brand copy.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Background application screen. |
title | string | How was your experience? | Sheet heading. |
subtitle | string | Your feedback helps us improve. | Supporting copy. |
thankYou | string | Thank you | Confirmation heading. |
confirmation | string | Your feedback has been sent. | Confirmation detail. |
submitLabel | string | Submit | Button text. |
initialRating | number | 2 | Initial rating, clamped to 1–5. |
labels | five-string tuple | Terrible … Loved it | Text for each integer rating. |
negativeReasons | readonly string[] | Slow service, Quality | Reasons below 4. |
positiveReasons | readonly string[] | Fast, Friendly, Great quality | Reasons at 4 or 5. |
fontFamily | string | system | Body font. |
mediumFontFamily | string | body font | Control font. |
titleFontFamily | string | system | Editorial heading font. |
colorScheme | light / dark / auto | auto | Follows device appearance by default. |
surfaceColor | string | light/dark warm neutral | Sheet fill. |
textColor | string | light/dark foreground | Text and button fill. |
initiallyOpen | boolean | true | Initial sheet visibility. |
springDamping | number | 28 | Rating and entrance spring damping. Values below the damping needed for ζ ≥ 0.8 are raised to that minimum. |
autoCloseMs | number | 1200 | Delay between submit and dismissal. |
onRatingChange | (rating: number) => void | — | Integer transitions. |
onSubmit | (rating: number, reasons: string[]) => void | — | One submission per opening; only current reasons are included. |
onClose | () => void | — | Called after dismissal finishes. |
accessibilityLabel | string | Rating | Adjustable control name. |
style | StyleProp<ViewStyle> | — | Root style, including flex: 1. |
testID | string | — | Root test identifier. |
The ref exposes open(), close(), reset(), submit(), and setRating(value, duration?). An optional duration sweeps continuously for scripted presentations; autoplay belongs to the demo.
Compatibility
Targets Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, Worklets 0.13.0, and Skia 2.14.0. Uses the installed Expo Go-compatible modules; no additional native package is introduced. iOS, Android, and web are intended targets. This piece needs Skia-aware lazy registration in a web catalog.
Accessibility
The track is an adjustable control with the current rating, label, range, and increment/decrement actions. Tapping the track is an alternative to dragging; tapping the handle dismisses the sheet. Reasons expose checkbox roles and checked states. Controls have at least 44-point hit areas, including the full-width slider hit zone. Background accessibility is hidden while the sheet is open.
Reduced motion replaces continuous expressions with integer expressions and a fade, removes blink, face tracking, jumps, nods, and sheet translation, and fades labels and chips without displacement. The confirmation is announced through a polite live region.
Demo timeline and layout
The Ember & Oat delivered-order screen uses the kit's flame tile and coffee, croissant, and cup-soda paths. The sheet's warm palette is independent of system appearance.
Autoplay resets at 120 ms, begins a continuous 2→5 sweep at 620 ms, reaches delight at 2,520 ms, submits at 3,320 ms, and begins dismissal 1,200 ms later. The cycle repeats every 5,800 ms. All demo timers are cleaned up.
At 390×844 the sheet begins at 0.38 H; at 320×694 it begins at 0.34 H to retain readable two-line supporting copy and touch targets. The face uses 0.40 W. The track centers at 0.75 H, reasons begin at 0.82 H, and Submit sits 18 points above the bottom. No demo control occupies the catalog viewer's top-right close area. Material effects use the face's radial shader and surface shadows, without a moving opaque overlay.



