About this animation
A mood arc that becomes an activity dashboard with rising pixel columns, with configurable mood labels, step data, palette, and fonts.
Why this one
An arc gives the thumb precise, one-point control instead of a row of faces or a straight slider. A translucent temperature tint makes the photograph respond to mood. Confirmation keeps the same number on screen as it flies into a saved check-in chip, connecting reflection with activity instead of cutting between screens. Pixel columns build from the ground up and regroup when the period changes.
How it works
onLayoutmeasures the host container. The dial uses the smaller of the available width and height; dashboard content scrolls in short containers.- An exclusive Gesture Handler pan/tap controls one continuous mood shared value. Release projects velocity into an integer spring target. Text rollers, the arc marker, temperature, and fading mood labels derive from that value.
- Each numeric column rolls only at its carry. Leading positions collapse, and commas have their own width and baseline. No React state updates run per frame.
- Confirmation animates a shared transition over 560 ms. The photograph moves upward while the same mood number shrinks into a coral chip. The dashboard fades and rises behind it.
- Pixels reveal bottom to top, one column at a time, with a 25 ms cadence. The activity total counts over 1.4 seconds. A second shared value rearranges persistent pixels for Week/Month.
- The demo starts at 52, moves to 64, confirms at 1.55 seconds, selects Month at 5.25 seconds and Week at 7.35 seconds, then resets at 10 seconds. Any touch stops its loop.
Install
npx expo install @shopify/react-native-skia expo-image expo-linear-gradient react-native-gesture-handler react-native-reanimated react-native-workletsUse a GestureHandlerRootView above this component and configure Reanimated/worklets for your Expo project. Web requires Skia's CanvasKit setup and a lazy Skia-aware host; load CanvasKit before evaluating this module. This folder imports no catalog or theme code. Icons: Lucide (ISC); glyph paths are bundled in icons.ts.
Usage
import { RingCheckinStats } from './ring-checkin-stats';
const steps = {
week: {
total: 11374,
days: [{ label: 'Mon', value: 11230 }, { label: 'Tue', value: 10240 }],
},
month: {
total: 48216,
days: [{ label: 'Week 1', value: 41230 }, { label: 'Week 2', value: 48216 }],
},
};
<RingCheckinStats
image={require('./ring.jpg')}
steps={steps}
onConfirm={saveMood}
/>const checkin = useRef<RingCheckinStatsRef>(null);
<RingCheckinStats
ref={checkin}
image={require('./ring.jpg')}
steps={steps}
question={['How does today feel?', 'Take a moment.']}
labels={{ detail: 'My ring', saved: 'Today’s check-in' }}
palette={{ background: '#201B19', surface: '#302825', ink: '#FFF5EF', muted: '#C8B8AF', border: '#493A33', pale: '#6D3F30' }}
pixelDelay={20}
transitionDuration={500}
testID="daily-mood"
/>
// Call from an event handler or effect.
checkin.current?.setValue(64);
checkin.current?.confirm();Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
image | ImageSourcePropType | Required | Portrait photo, preferably local. |
steps | { week: StepPeriod; month: StepPeriod } | Required | Independent totals and labeled column values; first 31 columns shown. Month may use weekly groups. |
question | [string, string] | Morning question | Two question lines. |
moodLabels | { max: number; label: string }[] | 30 Tense / 50 Okay / 70 Relaxed / 100 Energized | Ascending inclusive mood ranges. |
value | number | 52 | External mood changes; bounded to 0–100. Omit for internal control. |
onChange | (n: number) => void | — | Rounded mood updates on the JS thread. |
onConfirm | (n: number) => void | — | Confirmed mood. Skip also confirms the current mood. |
labels | Partial label object | Neutral English labels | skip, detail, stats, week, month, steps, saved, back, confirm, dial, decrease, increase, again. |
brand | ReactNode | — | Optional top-left identity. Aurora belongs to the demo only. |
palette | Partial<RingCheckinPalette> | Coral/light | background, surface, ink, muted, accent, pale, photoInk, cool, warm, shade, border. Override together for dark themes. |
fontFamily | string | System | Body and controls. |
lightFontFamily | string | System | Question and mood number. |
strongFontFamily | string | System | Second question line and steps. |
formatNumber | (n: number) => string | US locale | Chart annotations and announcements. Animated totals use US grouping. |
pixelDelay | number | 25 | Reveal cadence, milliseconds per square. |
transitionDuration | number | 560 | Confirmation motion duration in milliseconds. |
onInteraction | () => void | — | Stops host automation after touch. |
style | StyleProp<ViewStyle> | — | Host container styling. |
testID | string | ring-checkin-stats | Stable root and control suffixes. |
Ref: setValue(n), confirm(), setPeriod('week' | 'month'), reset().
Compatibility
Built against Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, worklets 0.13.0 and Skia 2.14.0. These native modules are included in the matching Expo Go runtime. Supports iOS, Android, and web.
The root fills its parent. Intended for a full-screen check-in or an embedded container, including 300×400; the host owns safe-area insets. The dashboard scrolls on short screens. The top-right 56 pt remains free of controls. The supplied 768×1344 artwork is demo-only and is below full-screen @3x density; supply a larger original for production. The demo uses the available loaded Geist Medium face for its strong type.
Accessibility
The dial is adjustable with increment/decrement actions and named tap/pan alternatives. Tapping the arc selects a point; the back button reduces the value by one. Buttons have at least 48×48 hit areas and stable test IDs. Confirmation and period selection announce their results. The chart exposes its data as readable labels; off-screen panels are hidden from accessibility.
Reduced motion removes the photo flight, number flight, pixel lift and regrouping arcs. Panels and labels fade over 200 ms, pixels fade together, and period layouts switch during a crossfade. Font scaling remains enabled.




