About this animation
A button with touch-linked highlights and a sweeping reflection, with configurable label, material colors, palette, and fonts.
Why this one
The light belongs to the button material. A rounded Skia clip confines both the radial focus and traveling reflection; the label renders above them. One sweep position drives the fill and edge light. A subtle 1280 ms idle reflection runs once every four seconds; reduced motion and disabled state suppress it. The Aster demo adds a savings goal, two material treatments, and complete Paper/Night themes.
How it works
onLayoutmeasures the actual button, independently of screen dimensions. Text stays in normal flow and can wrap at large font sizes.- Gesture Handler captures the normalized pressure location and starts the local radial focus in 100 ms. A separate spring scales the button to 0.97 with explicit mass.
- Touch-down advances one sweep position over 520 ms from the contact point toward the far edge. The default white center is 55% opaque without another dimming multiplier. A transparent gradient illuminates only the clipped fill, with a matching stroke lighting the edge. A quick tap still reaches peak halo brightness before fading within 520 ms of release.
- A new press cancels the previous sweep and starts at the new contact point, choosing the farther edge as its destination. Failed gestures restore pressure without firing the callback.
- Glass blurs geometric content behind the material using a local
BlurTargetView; Matte uses an opaque finish. The press halo brightens with the local focus and uses a real Skia blur and the shell has two shadow layers. shine()provides visual feedback without performing an action. Autoplay lives exclusively in the demo and stops on touch, including variant/theme controls.
Install
npx expo install @shopify/react-native-skia expo-blur react-native-gesture-handler react-native-reanimated react-native-workletsCopy this folder and mount the host app within GestureHandlerRootView. Configure Reanimated/Worklets for your Expo SDK. For web, initialize CanvasKit before mounting this component, following the installed Skia web setup. No assets or catalog imports are required by the component. Icons: Lucide (ISC), copied from the repository kit and used only by the demo.
Usage
import { TouchShineButton } from './touch-shine-button';
<TouchShineButton
label="Continue"
accessibilityLabel="Continue to the next step"
onPress={continueToNextStep}
style={{ flexGrow: 0, flexShrink: 0, flexBasis: 'auto' }}
/>const button = useRef<TouchShineButtonRef>(null);
<TouchShineButton
ref={button}
label="Create a goal"
accessibilityLabel="Create a savings goal"
onPress={createGoal}
colorScheme="light"
variant="matte"
shineDuration={480}
fontFamily="YourLoadedFont"
material={{ top: '#9775F5', bottom: '#6541D5', text: '#F4F4F8' }}
testID="goal"
style={{ flexGrow: 0, flexShrink: 0, flexBasis: 'auto' }}
/>
// Call from an event or effect, never during render:
button.current?.shine({ x: 0.2, y: 0.6 });
button.current?.reset();Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
label | string | Required | Visible label; empty/whitespace falls back to accessibilityLabel. |
onPress | () => void | Required | One synchronous callback per successful activation. |
accessibilityLabel | string | Required | Supply a meaningful, nonempty action label. |
disabled | boolean | false | Prevents touch, accessibility actions, and imperative shine. |
variant | 'glass' | 'matte' | 'glass' | Blur-backed or opaque material; unknown runtime values use Matte. |
shineDuration | number | 520 | Milliseconds, clamped to 100–2000; non-finite values use 520. |
colorScheme | 'light' | 'dark' | 'auto' | 'auto' | Selects semantic palette. The default branded CTA keeps light ink on dark violet in both themes. |
palette | TouchShinePalette | Scheme palette | All eight semantic colors: background, surface, text, muted, border, accent, success, error. The button uses accent for its halo; remaining roles are available to hosts. |
material | { top?, bottom?, text?, border?, reflection? } | Violet / light ink | Overrides every visible button material color independently of host semantics. Reflection defaults to #FFFFFF8C. |
fontFamily | string | System | Label font. |
titleFontFamily / monoFontFamily | string | Unset | Common API slots; unused because this component has no heading or numbers. |
style | StyleProp<ViewStyle> | Unset | Root style. Root has flex: 1; use { flexGrow: 0, flexShrink: 0, flexBasis: 'auto' } for intrinsic height, including wrapped labels. |
testID | string | 'touch-shine-button' | Root identifier; target is ${testID}-button. |
ref.shine(origin?) uses normalized 0–1 fractions, clamping finite out-of-range values and replacing non-finite coordinates with 0.5. Omitted origin is { x: 0.35, y: 0.5 }. It never calls onPress. ref.reset() cancels the sweep and returns to rest without a callback. A shine requested while disabled is ignored. There are no IDs, lists, asynchronous states, error messages, or session APIs to reconcile.
All component copy comes from label and accessibilityLabel; it has no internal status strings. The demo passes its full Copy object explicitly into GoalScreen, including theme/material controls and the action announcement. A successful demo action changes only the supporting message, with no overlapping status labels.
Compatibility
Built against 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. These modules are included in the project's Expo Go runtime; no new dependency is introduced. On Android 12+ Glass uses dimezisBlurViewSdk31Plus with its own blur target. Older Android versions keep the translucent fallback. Every blur wrapper has its own matching radius and clipping.
Demo timeline
A 5000 ms cycle rests for 600 ms, calls shine() at 600 and 1550 ms with opposite origins, rests after the second effect, and calls reset() at 3900 ms. Both effects finish before 3000 ms. Reset has at most 150 ms of fade (200 ms with reduced motion), followed by idle time until the next cycle. Real touch clears every pending timeout; unmount clears timers too. The component idle animation uses a UI-thread repeat with a 2720 ms rest and a 1280 ms subtle sweep; there is no JavaScript frame callback or interval. All animations are canceled on unmount.
Implementation notes
The label is in flow above the reflection, without animated duplicate text or an overlaid luminous block. The demo reserves its upper-right corner, uses a compact layout at 320 pt with scrolling reserved for short containers, and keeps a 480 pt content maximum on tablets. The first ref shine begins at 600 ms and completes within the first three seconds.
Accessibility
The component measures its own target with onLayout; its minimum height is 56 pt and it stretches to the available width. It applies no safe-area inset. The demo uses a maximum 480 pt width, normal flow without scrolling at 320×694 and 390×844, compact controls with minimum 48 pt targets. Below 740 pt in height, secondary copy is omitted and spacing is reduced; below 620 pt, a single full-screen outer scroll container with hidden indicators keeps every control reachable. The card fits its content; the reassurance stays in normal flow 10 pt below the button. Large text retains native font scaling with no fixed label height. The host owns safe areas and can place the component inside a 300×400 region or landscape screen.
The target exposes the button role, action label, disabled state, and screen-reader activation. On web, the focused target also handles Enter and Space without key-repeat activation. Reduced motion removes scaling and traveling reflections, replacing feedback with a stationary 200 ms focus fade. The demo announces goal creation and exposes selection state on its controls. There are no live announcements for decorative shine.




