Back to Components

Apple Intelligence–style Flame

Pull to refresh
15 days ago

About

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-skia
  • react-native-reanimated
  • react-native-screens (FullWindowOverlay on iOS)
ts
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)

tsx
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

  1. onScroll + scrollEventThrottle={16} so overscroll updates progress.
  2. Call onRefreshStart() when refresh begins, onRefreshEnd() when it finishes (always in finally).
  3. Mount <ScreenFlame /> only while visible is true (avoids idle GPU cost).

Manual progress (preview / demos)

tsx
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

PropTypeDefaultDescription
progressSharedValue<number>Required. 01 visibility / intensity.
intensitynumber0.95Overall brightness (03).
reachnumber64How far tongues reach inward from the bezel (px).
spreadnumber22Side / bottom glow width (px).
radiusnumber54Corner radius — match the device bezel.
speednumber0.32Animation speed multiplier.
styleStyleProp<ViewStyle>Extra style on the absolute root.

progress scales opacity, intensity, reach, sparks, and smoke. Prefer a smoothstep / withTiming ramp rather than hard 01 jumps.

useScreenFlamePull(thresholdPx?)

Drives progress from iOS overscroll and refresh lifecycle.

ArgDefaultDescription
thresholdPx92Pull distance (px) that maps to progress = 1.

Returns (ScreenFlamePull)

FieldTypeDescription
progressSharedValue<number>Pass to <ScreenFlame progress={…} />.
visiblebooleanMount the overlay while true.
onScroll(e) => voidAttach to ScrollView / FlatList.
onRefreshStart() => voidHold flame at full while refreshing.
onRefreshEnd() => voidFade 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

  1. 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.
  2. Host — iOS uses FullWindowOverlay so the effect sits on the true screen edge (above tabs / safe areas). Android uses an absolute fill View.
  3. Coords — Logical pixels (useWindowDimensions), not physical DPR — keeps the ring flush with the bezel.
  4. 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

GoalKnobs
Stronger pull responseLower thresholdPx (e.g. 64).
Longer tonguesHigher reach.
Softer / thinner rimLower spread + intensity.
Match iPhone bezelAdjust radius (~44–58 depending on device).
Slower flickerLower speed.

Keep progress animation short on appear (~150–200ms) and longer on dismiss (~600–800ms) so the exit feels intentional.


Platform notes

PlatformOverlay hostPull progress
iOSFullWindowOverlayNative rubber-band overscroll works well.
AndroidAbsolute fillOverscroll 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.

Posted by

E

eren

@eren