High-performance, fully customisable particle animations for Flutter. Starfields, snow, confetti, fireworks, connected webs, comets, burst explosions — with physics, touch/hover interaction, lifetime animations, trails, and burst emitters.
Live Demo · pub.dev · Issues · Contributing
flutter pub add particles_flutterimport 'package:particles_flutter/engine.dart';
import 'package:particles_flutter/shapes.dart';
Particles(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
boundType: BoundType.WrapAround,
particles: List.generate(80, (_) {
final rng = Random();
return CircularParticle(
radius: rng.nextDouble() * 4 + 1,
color: Colors.white.withValues(alpha: 0.7),
velocity: Offset(
(rng.nextDouble() - 0.5) * 60,
(rng.nextDouble() - 0.5) * 60,
),
);
}),
)Import: import 'package:particles_flutter/shapes.dart';
| Shape | Class |
|---|---|
| Circle | CircularParticle(radius, color, velocity) |
| Rectangle | RectangularParticle(width, height, color, velocity) |
| Rounded Rectangle | RoundRectangularParticle(width, height, cornerRadius, ...) |
| Triangle | TriangularParticle(width, height, color, velocity) |
| Oval | OvoidalParticle(width, height, color, velocity, rotationSpeed) |
| Image | ImageParticle(image, width, height, color, velocity) |
All shapes support rotationSpeed and all lifetime animation parameters.
All shapes accept these optional parameters. Omit any to keep default behavior.
CircularParticle(
color: Colors.yellow,
lifetime: 3.0,
// Two-color transition:
endColor: Colors.transparent,
// OR gradient across lifetime:
colorGradient: [Colors.white, Colors.yellow, Colors.orange, Colors.transparent],
colorCurve: Curves.linear,
...
)CircularParticle(
color: Colors.purple,
lifetime: 2.5,
startScale: 0.1,
endScale: 1.8,
scaleCurve: Curves.easeInOut,
...
)CircularParticle(
color: Colors.white,
lifetime: 2.0,
startOpacity: 1.0,
endOpacity: 0.0,
opacityCurve: Curves.easeIn,
...
)Tip: Set both
startOpacity: 0.0andendOpacity: 0.0for a triangle fade — particles fade in to full opacity at mid-life, then back out. No visible pop on spawn or death.
CircularParticle(
lifetime: 3.0,
startOpacity: 0.0, // fade in from invisible
endOpacity: 0.0, // fade out to invisible — triangle curve auto-applied
...
)CircularParticle(
color: Colors.cyan,
lifetime: 2.0,
trailEnabled: true,
trailLength: 7, // past positions to draw
trailFade: true, // fade older segments
...
)| Value | Behaviour |
|---|---|
BoundType.None |
Particles exit the canvas (default) |
BoundType.WrapAround |
Particles reappear from the opposite edge |
BoundType.Bounce |
Particles reflect off edges |
Particles(
boundType: BoundType.WrapAround,
...
)Import: import 'package:particles_flutter/interactions.dart';
Particles(
interaction: ParticleInteraction(
awayRadius: 120,
enableHover: true,
onTapAnimation: true,
awayAnimationDuration: Duration(milliseconds: 400),
awayAnimationCurve: Curves.easeOut,
hoverRadius: 80,
),
...
)Import: import 'package:particles_flutter/physics.dart';
Particles(
particlePhysics: ParticlePhysics(gravityScale: 30),
...
)Spawn particles from a fixed point — great for fountains and fireworks.
Particles(
boundType: BoundType.None,
particleEmitter: Emitter(
startPosition: Offset(width / 2, height / 2),
startPositionRadius: 10, // spawn scatter radius
clusterSize: 10, // particles per burst
delay: Duration(milliseconds: 300),
recycles: false, // true = loop forever
),
...
)Fire a fixed number of particles in a single burst — configurable spread, repeat interval, physics, and optional manual controller.
Import: import 'package:particles_flutter/engine.dart';
| Pattern | Description |
|---|---|
RadialBurst |
All directions evenly |
ConeBurst |
Within a configurable cone angle |
DirectionalBurst |
One direction with spread |
CustomBurst |
Offset Function(int index, int total) — build your own |
Particles(
particles: const [],
width: size.width,
height: size.height,
boundType: BoundType.None,
burstEmitters: [
BurstEmitter(
position: (size) => size.center(Offset.zero),
particleCount: 60,
pattern: RadialBurst(minSpeed: 150, maxSpeed: 400),
repeatCount: 1,
physics: ParticlePhysics(gravityScale: 120),
particleFactory: (i, total) => CircularParticle(
radius: 4,
color: Colors.orange,
velocity: Offset.zero,
lifetime: 1.8,
endOpacity: 0.0,
),
),
],
)BurstEmitter(
position: (size) => Offset(size.width / 2, size.height),
particleCount: 50,
pattern: ConeBurst(
angle: -pi / 2, // shoot upward
spread: pi / 2.5,
minSpeed: 300,
maxSpeed: 600,
),
repeatCount: 1,
positionRadius: 60,
physics: ParticlePhysics(gravityScale: 200),
particleFactory: (i, total) => RoundRectangularParticle(
width: 12, height: 4, cornerRadius: 2,
color: Colors.pink,
velocity: Offset.zero,
rotationSpeed: 3.0,
lifetime: 2.0,
endOpacity: 0.0,
),
)// In your State:
final _ctrl = BurstEmitterController();
Offset _tapPos = Offset.zero;
// In build:
Listener(
behavior: HitTestBehavior.opaque,
onPointerDown: (event) {
_tapPos = event.localPosition;
_ctrl.trigger();
},
child: Particles(
particles: const [],
width: size.width,
height: size.height,
boundType: BoundType.None,
burstEmitters: [
BurstEmitter(
position: (size) => _tapPos,
particleCount: 40,
pattern: RadialBurst(minSpeed: 80, maxSpeed: 240),
repeatCount: 0, // 0 = fire only when triggered
controller: _ctrl,
physics: ParticlePhysics(gravityScale: 80),
particleFactory: (i, total) => CircularParticle(
radius: 3,
color: Colors.cyan,
velocity: Offset.zero,
lifetime: 1.2,
endScale: 0.0,
endOpacity: 0.0,
),
),
],
),
)| Parameter | Type | Default | Description |
|---|---|---|---|
position |
Offset Function(Size) |
required | Burst origin |
particleCount |
int |
required | Particles per burst |
particleFactory |
Particle Function(int, int) |
required | Builds each particle |
pattern |
BurstPattern |
required | Velocity spread strategy |
initialDelay |
Duration |
Duration.zero |
Delay before first burst |
repeatCount |
int |
1 |
Bursts to fire; 0 = controller-only |
repeatInterval |
Duration |
Duration.zero |
Gap between repeats |
positionRadius |
double |
0 |
Scatter radius around position |
physics |
ParticlePhysics? |
null |
Gravity for burst particles |
enableTrails |
bool |
false |
Trail rendering (expensive at high counts) |
controller |
BurstEmitterController? |
null |
Manual trigger |
maxPoolSize |
int |
500 |
Memory ceiling; oldest particles reclaimed when full |
Emitter.burst(...)is a shorthand factory that returns aBurstEmitter— use whichever style you prefer.
Particles(
boundType: BoundType.WrapAround,
interaction: ParticleInteraction(awayRadius: 120, enableHover: true),
particles: List.generate(120, (_) => CircularParticle(
radius: Random().nextDouble() * 3 + 0.5,
color: Colors.white.withValues(alpha: 0.7),
velocity: Offset((Random().nextDouble() - 0.5) * 40,
(Random().nextDouble() - 0.5) * 40),
)),
...
)Particles(
boundType: BoundType.WrapAround,
particlePhysics: ParticlePhysics(gravityScale: 20),
particles: List.generate(100, (_) => CircularParticle(
radius: Random().nextDouble() * 6 + 2,
color: Colors.white.withValues(alpha: 0.8),
velocity: Offset((Random().nextDouble() - 0.5) * 20,
Random().nextDouble() * 15 + 5),
)),
...
)Particles(
boundType: BoundType.None,
particlePhysics: ParticlePhysics(gravityScale: 45),
particleEmitter: Emitter(
startPosition: Offset(width / 2, height / 2),
startPositionRadius: width * 0.25,
clusterSize: 15,
delay: Duration(milliseconds: 300),
),
particles: List.generate(150, (_) {
final angle = Random().nextDouble() * 2 * pi;
final speed = Random().nextDouble() * 120 + 60;
return TriangularParticle(
width: 6, height: 6,
color: Colors.orange,
velocity: Offset(cos(angle) * speed, sin(angle) * speed),
rotationSpeed: 3.0,
);
}),
...
)Particles(
boundType: BoundType.WrapAround,
particles: List.generate(80, (_) {
final angle = Random().nextDouble() * 2 * pi;
final speed = Random().nextDouble() * 60 + 40;
return CircularParticle(
radius: Random().nextDouble() * 3 + 2,
color: Colors.white,
velocity: Offset(cos(angle) * speed, sin(angle) * speed),
lifetime: Random().nextDouble() * 2.0 + 1.5,
colorGradient: [Colors.white, Colors.yellow, Colors.orange, Colors.transparent],
startOpacity: 0.0,
endOpacity: 0.0,
trailEnabled: true,
trailLength: 7,
trailFade: true,
);
}),
...
)Particles(
boundType: BoundType.WrapAround,
particles: List.generate(60, (_) => CircularParticle(
radius: 10,
color: Color(0xFF7C4DFF),
velocity: Offset((Random().nextDouble() - 0.5) * 20,
(Random().nextDouble() - 0.5) * 20),
lifetime: 2.5,
startScale: 0.1,
endScale: 1.8,
scaleCurve: Curves.easeInOut,
startOpacity: 0.0,
endOpacity: 0.0,
)),
...
)Particles(
boundType: BoundType.WrapAround,
particles: List.generate(50, (_) => CircularParticle(
radius: Random().nextDouble() * 14 + 8,
color: Color(0xFF69F0AE),
velocity: Offset((Random().nextDouble() - 0.5) * 15,
(Random().nextDouble() - 0.5) * 10),
lifetime: Random().nextDouble() * 3.0 + 2.0,
startOpacity: 0.0,
endOpacity: 0.0,
startScale: 0.6,
endScale: 1.2,
scaleCurve: Curves.easeOut,
)),
...
)Particles(
boundType: BoundType.None,
particlePhysics: ParticlePhysics(gravityScale: 40),
particles: List.generate(150, (_) {
final angle = Random().nextDouble() * 2 * pi;
final speed = Random().nextDouble() * 80 + 80;
return CircularParticle(
radius: Random().nextDouble() * 4 + 2,
color: Colors.yellow,
velocity: Offset(cos(angle) * speed, sin(angle) * speed),
lifetime: Random().nextDouble() * 1.0 + 1.2,
colorGradient: [Colors.white, Colors.yellow, Colors.red, Colors.transparent],
startScale: 1.0,
endScale: 0.0,
scaleCurve: Curves.easeIn,
startOpacity: 0.0,
endOpacity: 0.0,
trailEnabled: true,
trailLength: 6,
trailFade: true,
);
}),
...
)final ctrl = BurstEmitterController();
Offset tapPos = Offset.zero;
Listener(
behavior: HitTestBehavior.opaque,
onPointerDown: (e) { tapPos = e.localPosition; ctrl.trigger(); },
child: Particles(
particles: const [],
width: size.width,
height: size.height,
boundType: BoundType.None,
burstEmitters: [
BurstEmitter(
position: (size) => tapPos,
particleCount: 40,
pattern: RadialBurst(minSpeed: 100, maxSpeed: 300),
repeatCount: 0,
controller: ctrl,
physics: ParticlePhysics(gravityScale: 80),
particleFactory: (i, _) => CircularParticle(
radius: Random().nextDouble() * 3 + 1.5,
color: Colors.orange,
velocity: Offset.zero,
lifetime: 1.2,
endScale: 0.0,
endOpacity: 0.0,
),
),
],
),
)See all scenes running live → particles-flutter.vercel.app
- Breaking: Dart SDK
>=3.0.0required (Flutter 3.10+). Projects on Dart 2.x must upgrade first. - BurstEmitter — fire fixed particle counts in radial, cone, directional, or custom spread patterns
- BurstEmitterController — trigger bursts manually from gestures, game events, or any code
- Tap-to-burst —
BurstEmitterController.trigger()+onPointerDownfor per-tap explosions - Overlap-safe pooling — multiple bursts in flight simultaneously; memory hard-capped at
maxPoolSize
- Color over lifetime — smooth two-color or gradient transitions
- Scale over lifetime — grow/shrink with curve support
- Fade over lifetime — fade in, out, or triangle (both ends zero = auto mid-peak)
- Particle trails — motion trails with configurable length and fade
- Object pooling for ParticleLine — reduced GC pressure on line-connected scenes
- Performance improvements — touch interaction, physics, and emitter update loops
All releases are fully backward compatible — no changes needed to existing code.
If this package saved you time:
Bug reports and pull requests welcome.
- Found a bug? → Open an issue
- Have a fix? → Send a PR
- Want to chat? → Twitter @rajajain08





