Skip to content

Instantly share code, notes, and snippets.

@vanGalilea
Created November 20, 2025 09:39
Show Gist options
  • Select an option

  • Save vanGalilea/089a6e500435047ec4d94fb3b7b52829 to your computer and use it in GitHub Desktop.

Select an option

Save vanGalilea/089a6e500435047ec4d94fb3b7b52829 to your computer and use it in GitHub Desktop.
A fully custom, physics-driven confetti engine for React Native + Expo, built with Reanimated + SVG. 8 shapes, randomized trajectories, gravity simulation, peak arcs, opacity fades. Everything you need to instantly upgrade your success screens with joyful, performant confetti.
import React, { useEffect } from "react";
import { Dimensions, View } from "react-native";
import Animated, {
Easing,
useAnimatedStyle,
useSharedValue,
withDelay,
withSequence,
withTiming,
} from "react-native-reanimated";
import Svg, { Circle, Polygon, Rect } from "react-native-svg";
import * as R from "remeda";
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("window");
// Available confetti colors
const COLORS = ["#1b70de", "#32af4b"] as const;
// Gravity fall duration after reaching peak (ms)
const FALL_DURATION = 5500;
// Opacity fade-in duration (ms)
const OPACITY_FADE_DURATION = 80;
// Allowed number of confetti pieces
const PIECES_COUNT = 250;
// Size range for confetti shapes
const MIN_SIZE = 6;
const MAX_SIZE = 12;
// Angle range of the confetti cannon (degrees)
const SHOOT_ANGLE_MIN = -15;
const SHOOT_ANGLE_MAX = 15;
// Upward velocity range (affects peak height)
const VELOCITY_MIN = 1400;
const VELOCITY_MAX = 2200;
// Range for opacity
const OPACITY_MIN = 25;
const OPACITY_MAX = 90;
// Allowed peak Y range (higher peak = lower visual height)
const PEAK_MIN = SCREEN_HEIGHT * 0.05;
const PEAK_MAX = SCREEN_HEIGHT * 0.65;
// Horizontal drift during falling phase
const DRIFT_MIN = -30;
const DRIFT_MAX = 30;
// Upward movement duration (ms)
const UP_MIN_DURATION = 1000;
const UP_MAX_DURATION = 1600;
/**
* Converts normalized polygon coordinates to actual SVG size
*/
const scalePoints = (points: string, size: number) =>
points
.split(" ")
.map((p) => {
const [x, y] = p.split(",").map(Number);
return `${(x / 100) * size},${(y / 100) * size}`;
})
.join(" ");
type ShapeProps = {
type: number;
size: number;
color: string;
opacity: number;
};
/**
* Renders different SVG shapes for each confetti type.
*/
const ConfettiShape: React.FC<ShapeProps> = ({
type,
size,
color,
opacity,
}) => {
switch (type) {
case 0:
return (
<Circle
cx={size / 2}
cy={size / 2}
r={size / 2}
fill={color}
opacity={opacity}
/>
);
case 1:
return <Rect width={size} height={size} fill={color} opacity={opacity} />;
case 2:
return (
<Rect
width={size * 2.8}
height={size * 0.8}
fill={color}
opacity={opacity}
/>
);
case 3:
return (
<Polygon
points={`0,${size} ${size / 2},0 ${size},${size}`}
fill={color}
opacity={opacity}
/>
);
case 4:
return (
<Polygon
points={scalePoints("50,8 92,35 80,80 20,80 8,35", size)}
fill={color}
opacity={opacity}
/>
);
case 5:
return (
<Polygon
points={scalePoints("50,0 95,25 95,75 50,100 5,75 5,25", size)}
fill={color}
opacity={opacity}
/>
);
case 6:
return (
<Polygon
points={scalePoints(
"50,0 65,35 98,40 70,60 80,95 50,75 20,95 30,60 2,40 35,35",
size,
)}
fill={color}
opacity={opacity}
/>
);
case 7:
return (
<Polygon
points={scalePoints(
"50,0 65,30 95,35 70,60 80,95 50,75 20,95 30,60 5,35 35,30",
size,
)}
fill={color}
opacity={opacity}
/>
);
default:
return null;
}
};
/**
* One falling & rotating confetti particle.
* Handles:
* - upward launch
* - peak arc
* - gravity fall
* - rotation
* - opacity fade-in
*/
const ConfettiPiece = ({ delay }: { delay: number }) => {
const y = useSharedValue(SCREEN_HEIGHT + 100);
const x = useSharedValue(SCREEN_WIDTH / 2);
const rotate = useSharedValue(0);
const opacity = useSharedValue(0);
// Randomized properties per piece
const size = R.randomInteger(MIN_SIZE, MAX_SIZE);
const color = COLORS[R.randomInteger(0, COLORS.length - 1)];
const op = R.randomInteger(OPACITY_MIN, OPACITY_MAX) / 100;
const type = R.randomInteger(0, 7);
// Shooting angle
const angleDeg = R.randomInteger(SHOOT_ANGLE_MIN, SHOOT_ANGLE_MAX);
const angleRad = (angleDeg * Math.PI) / 180;
// Upward velocity controls peak height
const velocity = R.randomInteger(VELOCITY_MIN, VELOCITY_MAX);
const velX = velocity * Math.sin(angleRad);
// Vertical peak range
const peakY = R.randomInteger(PEAK_MIN, PEAK_MAX);
// Horizontal distance reached at peak
const peakX = SCREEN_WIDTH / 2 + velX * 0.5;
// Gentle horizontal drifting during fall
const fallDriftX = R.randomInteger(DRIFT_MIN, DRIFT_MAX);
// Upward movement duration varies per piece
const upDuration = R.randomInteger(UP_MIN_DURATION, UP_MAX_DURATION);
useEffect(() => {
// Fade in quickly
opacity.value = withTiming(op, { duration: OPACITY_FADE_DURATION });
// Vertical movement: launch → peak → fall
y.value = withDelay(
delay,
withSequence(
withTiming(peakY, {
duration: upDuration,
easing: Easing.out(Easing.cubic),
}),
withTiming(SCREEN_HEIGHT + 500, {
duration: FALL_DURATION,
easing: Easing.in(Easing.quad),
}),
),
);
// Horizontal movement: cone spread → drift
x.value = withDelay(
delay,
withSequence(
withTiming(peakX, {
duration: upDuration,
easing: Easing.out(Easing.cubic),
}),
withTiming(peakX + fallDriftX, {
duration: FALL_DURATION,
easing: Easing.linear,
}),
),
);
// Continuous rotation
rotate.value = withDelay(
delay,
withTiming(R.randomInteger(-2200, 2200), {
duration: upDuration + FALL_DURATION,
easing: Easing.linear,
}),
);
}, [delay]);
// Apply animated transforms
const style = useAnimatedStyle(() => ({
opacity: opacity.value,
transform: [
{ translateX: x.value },
{ translateY: y.value },
{ rotate: `${rotate.value}deg` },
],
}));
return (
<Animated.View style={style} className="absolute">
<Svg height={size * 2.6} width={size * 4.2}>
<ConfettiShape type={type} size={size} color={color} opacity={op} />
</Svg>
</Animated.View>
);
};
/**
* Full screen confetti animation that fires upon mount
*/
export const Confetti = () => (
<View pointerEvents="none" className="absolute-fill">
{Array.from({ length: PIECES_COUNT }, (_, i) => (
<ConfettiPiece key={i} delay={i * 4} />
))}
</View>
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment