About this animation
A product reveal with an orbiting spotlight and touch parallax, with configurable product image, copy, accent color, and fonts.
Why this one
A static product photo cannot invite exploration. Here the light belongs to the product pixels: the unlit surface stays at 22% brightness, the spotlight reveals its material, and a floor reflection gives it volume. Moving the light shifts the shadow, halo, product, and highlight at different depths. The Tidewell demo uses original local artwork and a kit identity.
How it works
- Load a transparent local image before starting a 2,400 ms reveal. One intro progress drives a 1,600 ms orbit with the
(0.77, 0, 0.175, 1)curve, followed by an 800 ms opening of the light. - Draw a perspective floor, two contact-shadow layers, a blurred coral halo, the product, and an inverted reflection in one Skia Canvas.
- Sample the image with a runtime shader. Multiply its RGB by a soft radial light, preserving its alpha. No rectangle obscures the product. The reflection uses the same shader with a vertical fade.
- Track a continuous focal position using interruptible
{ damping: 52, stiffness: 260 }(effective mass 4) springs. Apply opposite shadow/halo parallax, forward product parallax, and perspective rotations capped at approximately 10°/6° at the default intensity. Release springs receive the gesture velocity. - Tint the neutral mesh with a ColorMatrix inside a neutral-pixel mask, leaving coral pixels in their original color. Sweep a narrow light through the product in 450 ms. After four seconds without interaction, a gentle ten-second orbit resumes.
- Scale the purchase button independently and draw the kit checkmark on confirmation. Gesture Handler handles both the drag and the alternative tap.
Install
npx expo install @shopify/react-native-skia react-native-gesture-handler react-native-reanimated react-native-workletsCopy this folder without demo/ into your app. Place the screen under GestureHandlerRootView. Configure Reanimated and worklets using your Expo SDK's recommended setup. On web, load CanvasKit before importing the component, for example with Skia's WithSkiaWeb boundary and a lazy component import. Do not import this component statically before Skia's web initialization.
Usage
import { SpotlightProductStage } from './spotlight-product-stage';
<SpotlightProductStage source={require('./assets/product.png')} />;import { useRef } from 'react';
import { SpotlightProductStage, type SpotlightProductStageRef } from './spotlight-product-stage';
const stage = useRef<SpotlightProductStageRef>(null);
<SpotlightProductStage
ref={stage}
source={require('./assets/product.png')}
eyebrow="NEW RELEASE"
title="Weightless"
subtitle="on purpose."
actionLabel="Add to bag — $160"
accentColor="#FB7185"
parallaxIntensity={0.8}
spotlightRadius={0.32}
fontFamily="YourInterfaceFont"
titleFontFamily="YourEditorialFont"
italicFontFamily="YourEditorialItalicFont"
onAddToBag={() => addProductToCart()}
/>;
// Call from an event handler, never during render.
stage.current?.replay();Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
source | DataSourceParam | Required | Transparent product image; local assets recommended |
eyebrow | string | NEW RELEASE | Launch label |
title / subtitle | string | Made to move. / Made for you. | Editorial headline |
actionLabel / addedLabel | string | Add to bag / Added | Purchase button labels |
hintLabel / footerLabel | string | Exploration instruction / empty | Supporting copy |
brandName / brandGlyph | string | None | Optional wordmark and kit SVG glyph |
brandColors | [string, string] | Teal gradient | Brand tile colors |
backgroundColor | string | #07090C | Stage backdrop |
accentColor / subtitleColor | string | #FB7185 / #FDA4AF | Halo and editorial accent |
productWidth | number | 0.86 | Fraction of measured container width, clamped to 0.5–0.94 |
revealDuration | number | 2400 | Intro duration in ms, minimum 800 |
spotlightRadius | number | 0.36 | Radius as a fraction of container width |
parallaxIntensity | number | 1 | Depth movement multiplier |
fontFamily | string | System | Interface font |
titleFontFamily / italicFontFamily | string | System / title font | Editorial fonts |
monoFontFamily | string | System | Small labels |
accessibilityLabel | string | Explore product lighting | Exploration area description |
onStateChange | (state) => void | None | dark, revealed, or exploring |
onColorChange | (index: number) => void | None | Selected color callback |
onAddToBag | () => void | None | Purchase callback |
style / testID | StyleProp<ViewStyle> / string | None | Container overrides and test identifier |
The ref exposes replay(), reveal(), orbit(), selectColor(index), and addToBag(). Color indices are pearl/coral (0), midnight/coral (1), and sage/coral (2). Replay resets purchase confirmation. Autoplay orchestration belongs to the demo, not the public component.
Compatibility
Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, worklets 0.13.0, Gesture Handler 3.3, and Skia 2.14.0. The installed SDK 58 Expo Go stack includes these native modules. A compatible development build also works. Supports iOS, Android, and web. Use a portrait container, with 320×694 as the smallest demo layout target. Longer custom copy should be reviewed in your own layout.
Accessibility
Reduced motion presents the product fully lit with no orbit, perspective movement, parallax, sweep, or button rebound. Color changes transition in 200 ms. Drag exploration also has a tap-to-reveal alternative and an accessibility activation action. Color controls expose radio roles and selected states with 44 pt targets. The primary action has a button role, a confirmation label, and a 56 pt target.
Icons: Lucide (ISC). The demo's Tidewell identity comes from the repository brand kit. The public component has no theme or demo imports.




