About this animation
An achievement reveal with a tilting badge and configurable copy, fonts, accent color, and background.
Why this one
Most achievement screens show a static image and generic confetti. This badge is vector geometry: a rounded hexagonal medallion with a beveled face, engraved Lucide sparkles, and a metallic ribbon. Heat travels around the edge and inward through the material. Fourteen gold and violet sparks celebrate the final turn, and dragging the badge changes its angle and reflection.
How it works
- Measure the container with
onLayout; size the medallion to 56% of its width and center it at 36% of its height. - Animate one normalized sequence value over 2,100 ms. Derive forging (0–600 ms), a curved perspective turn (600–1,400 ms), sparks and a light ring (1,400–1,900 ms), and staggered content (1,500–2,100 ms).
- Render two metallic gradients inside the vector silhouette. A blurred dark core contracts to reveal the gold from the outside in; an incandescent trimmed path follows the edge.
- Project the turn with perspective and cosine-based narrowing. Darken the rear and hide the engraving while it faces away.
- Compose pan and tap with
Gesture.Exclusive. Two continuous tilt values steer the surface and gradient, then return with interruptible springs. Pressure feedback uses its own value. - Keep ambient reflection and floating cycles on the UI thread. Share uses the native share sheet by default; Continue fades and scales out before calling your callback.
Install
npx expo install @shopify/react-native-skia@2.14.0 expo-blur@~58.0.5 react-native-gesture-handler@~3.3.0 react-native-reanimated@4.7.1 react-native-worklets@0.13.0Wrap your app in GestureHandlerRootView. For web, initialize CanvasKit before mounting this component, following your project's Skia web setup. The catalog host loads Skia lazily; this folder does not configure your host. No assets or fonts are fetched by the component.
Usage
import AchievementUnlock from './achievement-unlock';
export function MilestoneScreen() {
return (
<AchievementUnlock
title="30-day streak"
body="You showed up every day this month."
onContinue={() => closeMilestone()}
/>
);
}Pass your loaded font names and tune the badge and timing:
const achievement = useRef<AchievementUnlockRef>(null);
<AchievementUnlock
ref={achievement}
title="A month of progress"
badgeLabel="30"
accentColor="#A78BFA"
badgeWidth={0.6}
tiltLimit={20}
duration={2300}
fontFamily="YourInterfaceFont"
titleFontFamily="YourEditorialFont"
monoFontFamily="YourMonoFont"
nextAchievements={['60 days', '90 days', 'One year']}
onShare={() => openYourShareFlow()}
/>;
// Replay from a parent button or an external achievement event.
achievement.current?.enter();Import AchievementUnlockRef as a type from the folder. The component enters automatically on mount. Keep callback identities stable to avoid restarting the entry effect. The demo owns its 5.2-second autoplay loop and Aurora branding.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| title | string | Achievement unlocked | Editorial heading; designed for one short line. |
| body | string | A little progress, every day. | Supporting copy; designed for two lines. |
| eyebrow | string | ACHIEVEMENT UNLOCKED | Small uppercase heading. |
| badgeLabel | string | 30 | Short engraved ribbon figure. |
| nextAchievements | readonly string[] | ['60 days', '90 days', 'One year'] | Up to three locked milestone labels. |
| header | ReactNode | — | Optional brand content at 9% of container height. |
| accentColor | string | #8B5CF6 | Ambient light color. |
| backgroundColor | string | #0B0716 | Background color. |
| duration | number | 2100 | Complete sequence duration; minimum 300 ms. |
| badgeWidth | number | 0.56 | Badge width fraction, clamped to 0.4–0.7. |
| tiltLimit | number | 25 | Maximum tilt in degrees, clamped to 0–35. |
| fontFamily | string | System | Interface typeface. |
| titleFontFamily | string | System | Editorial typeface. |
| monoFontFamily | string | System | Ribbon, eyebrow, and milestone typeface. |
| shareLabel | string | Share | Share button text and accessible label. |
| continueLabel | string | Continue | Exit button text and accessible label. |
| onShare | () => void | Native share sheet | Custom sharing handler. |
| onContinue | () => void | — | Called after exit completes. |
| onUnlock | () => void | — | Called after a complete entry finishes. |
| accessibilityLabel | string | title | Achievement text announced after entry. |
| style | StyleProp<ViewStyle> | — | Root container override; root has flex: 1. |
| testID | string | — | Root test identifier. |
Ref methods: enter() restarts the complete sequence, replay() repeats the turn and sparks while preserving revealed copy, and dismiss() exits and calls onContinue.
Compatibility
Built for Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, Worklets 0.13.0, Gesture Handler 3.3, Skia 2.14.0, and Expo Blur 58.0.5. Uses Expo Go's bundled modules; no extra native dependency was added. The Share button samples a BlurTargetView on Android SDK 31+ and uses the supported tinted fallback on older Android versions.
Supports iOS, Android, and web. Text blur runs on web and Android; iOS uses the same staggered opacity and translation because React Native does not provide iOS text-filter blur.
Accessibility
Interactive controls have button roles and descriptive labels; buttons are at least 44 pt tall. The badge exposes an accessibility activation action to replay, in addition to its Gesture Handler tap. A separate 44 pt replay control supports keyboard and screen-reader activation. Locked milestones have readable spoken descriptions. Entry announces “Achievement unlocked” followed by the achievement label. Reduced motion starts with fully forged gold, removes rotation, sparks, floating, and tilt, and uses a 200 ms text fade. Exit uses a 150 ms fade. Sharing errors are announced without logging user data.
Layout and performance
The demo reserves the upper-right corner for the catalog viewer's close control. Layout uses container fractions, with smaller body and heading sizes below 350 pt width. Four Canvas instances serve the demo (background/particles, badge, locked icons, brand); the reusable component uses three. Reflections, particles, text reveal, and gesture response update on the UI thread without per-frame React state. The glow, heat, and reflection belong to the material; no rectangular highlight covers the text.
Icons: Lucide (ISC). Paths are copied from the Native Animation kit into icons.ts.




