ScreenFlame
Full-screen bezel flame overlay — Apple Intelligence–style multicolor fire that roots on the device edge and burns inward. Built with Skia (SkSL) + Reanimated. Single file: shader, overlay, and pull-to-refresh hook.
components/effects/screen-flame.tsx
Install / imports
Peer deps (already in the mobile app):
@shopify/react-native-skiareact-native-reanimatedreact-native-screens(FullWindowOverlayon iOS)
import {
ScreenFlame,
useScreenFlamePull,
type ScreenFlameProps,
type ScreenFlamePull,
} from '@/components/effects/screen-flame';
// or from the barrel
import { ScreenFlame, useScreenFlamePull } from '@/components/effects';Quick start (pull-to-refresh)
function HomeScreen() {
const {
progress,
visible,
onScroll,
onRefreshStart,
onRefreshEnd,
} = useScreenFlamePull();
async function onRefresh() {
onRefreshStart();
try {
await refetch();
} finally {
onRefreshEnd();
}
}
return (
<View style={{ flex: 1 }}>
<ScrollView
onScroll={onScroll}
scrollEventThrottle={16}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}>
{/* content */}
</ScrollView>
{visible ? <ScreenFlame progress={progress} /> : null}
</View>
);
}Wire-up checklist
onScroll+scrollEventThrottle={16}so overscroll updates progress.- Call
onRefreshStart()when refresh begins,onRefreshEnd()when it finishes (always infinally). - Mount
<ScreenFlame />only whilevisibleis true (avoids idle GPU cost).
Manual progress (preview / demos)
import { useSharedValue, withTiming, Easing } from 'react-native-reanimated';
import { ScreenFlame } from '@/components/effects/screen-flame';
function Preview() {
const progress = useSharedValue(0);
return (
<>
<Button
title="Ignite"
onPress={() => {
progress.value = withTiming(1, {
duration: 400,
easing: Easing.out(Easing.cubic),
});
}}
/>
<ScreenFlame progress={progress} />
</>
);
}In-app preview: Co-founder → Flame preview (app/(tabs)/cofounder/flame-preview.tsx).
API
ScreenFlame
| Prop | Type | Default | Description |
|---|---|---|---|
progress | SharedValue<number> | — | Required. 0–1 visibility / intensity. |
intensity | number | 0.95 | Overall brightness (0–3). |
reach | number | 64 | How far tongues reach inward from the bezel (px). |
spread | number | 22 | Side / bottom glow width (px). |
radius | number | 54 | Corner radius — match the device bezel. |
speed | number | 0.32 | Animation speed multiplier. |
style | StyleProp<ViewStyle> | — | Extra style on the absolute root. |
progress scales opacity, intensity, reach, sparks, and smoke. Prefer a smoothstep / withTiming ramp rather than hard 0 ↔ 1 jumps.
useScreenFlamePull(thresholdPx?)
Drives progress from iOS overscroll and refresh lifecycle.
| Arg | Default | Description |
|---|---|---|
thresholdPx | 92 | Pull distance (px) that maps to progress = 1. |
Returns (ScreenFlamePull)
| Field | Type | Description |
|---|---|---|
progress | SharedValue<number> | Pass to <ScreenFlame progress={…} />. |
visible | boolean | Mount the overlay while true. |
onScroll | (e) => void | Attach to ScrollView / FlatList. |
onRefreshStart | () => void | Hold flame at full while refreshing. |
onRefreshEnd | () => void | Fade out (~700ms), then set visible false. |
Behavior
- Overscroll:
progress = clamp((-contentOffset.y) / thresholdPx, 0, 1). - While refreshing, scroll updates are ignored (progress held via timing).
- Fade-out duration: 700ms.
How it works
- Shader — SkSL runtime effect draws flame only near a rounded-rect matching the screen. Roots sit on the bezel (
distance ≈ 0); tongues grow inward. Palette sweeps blue → indigo → pink → orange → cyan. - Host — iOS uses
FullWindowOverlayso the effect sits on the true screen edge (above tabs / safe areas). Android uses an absolute fillView. - Coords — Logical pixels (
useWindowDimensions), not physical DPR — keeps the ring flush with the bezel. - Pointer events — Overlay is
pointerEvents="none"; it never steals touches.
Related but separate: FlameWrap / flame-shader.ts burn content (e.g. a profile card). ScreenFlame has no content sampler — screen edges only.
Tuning tips
| Goal | Knobs |
|---|---|
| Stronger pull response | Lower thresholdPx (e.g. 64). |
| Longer tongues | Higher reach. |
| Softer / thinner rim | Lower spread + intensity. |
| Match iPhone bezel | Adjust radius (~44–58 depending on device). |
| Slower flicker | Lower speed. |
Keep progress animation short on appear (~150–200ms) and longer on dismiss (~600–800ms) so the exit feels intentional.
Platform notes
| Platform | Overlay host | Pull progress |
|---|---|---|
| iOS | FullWindowOverlay | Native rubber-band overscroll works well. |
| Android | Absolute fill | Overscroll is limited; flame mostly appears via onRefreshStart / onRefreshEnd. |
Requires a dev client / custom native build with Skia — not Expo Go unless Skia is included in your binary.