flutter_confetti_plus 0.0.2 copy "flutter_confetti_plus: ^0.0.2" to clipboard
flutter_confetti_plus: ^0.0.2 copied to clipboard

A Flutter confetti package with quick overlays, controller-based widgets, emoji particles, widget sprites, and Konfetti-style party presets.

example/lib/main.dart

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_confetti_plus/flutter_confetti_plus.dart';

void main() => runApp(const ConfettiSample());

class ConfettiSample extends StatelessWidget {
  const ConfettiSample({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Confetti Plus',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        brightness: Brightness.dark,
        colorSchemeSeed: const Color(0xFF2DD4BF),
        useMaterial3: true,
      ),
      home: const ConfettiDemoPage(),
    );
  }
}

class ConfettiDemoPage extends StatefulWidget {
  const ConfettiDemoPage({super.key});

  @override
  State<ConfettiDemoPage> createState() => _ConfettiDemoPageState();
}

class _ConfettiDemoPageState extends State<ConfettiDemoPage> {
  late final ConfettiController _centerController;
  late final ConfettiController _rightController;
  late final ConfettiController _leftController;
  late final ConfettiController _topController;
  late final ConfettiController _bottomController;

  final TextEditingController _emojiController = TextEditingController();
  final TextEditingController _spriteController = TextEditingController();

  final List<String> _emojis = ['🎉', '🔥', '🚀', '💰'];
  final List<Widget> _widgetSprites = [
    const _SpriteBadge(
      label: 'A',
      icon: Icons.rocket_launch,
      color: Color(0xFF38BDF8),
    ),
    const _SpriteBadge(
      label: 'B',
      icon: Icons.local_fire_department,
      color: Color(0xFFFB7185),
    ),
    const _SpriteBadge(label: 'C', icon: Icons.star, color: Color(0xFFFACC15)),
  ];

  @override
  void initState() {
    super.initState();
    _centerController = ConfettiController(
      duration: const Duration(seconds: 10),
    );
    _rightController = ConfettiController(
      duration: const Duration(seconds: 10),
    );
    _leftController = ConfettiController(duration: const Duration(seconds: 10));
    _topController = ConfettiController(duration: const Duration(seconds: 10));
    _bottomController = ConfettiController(
      duration: const Duration(seconds: 10),
    );
  }

  @override
  void dispose() {
    _centerController.dispose();
    _rightController.dispose();
    _leftController.dispose();
    _topController.dispose();
    _bottomController.dispose();
    _emojiController.dispose();
    _spriteController.dispose();
    super.dispose();
  }

  void _addEmojis([String? value]) {
    final input = (value ?? _emojiController.text).trim();
    if (input.isEmpty) return;

    setState(() {
      _emojis.addAll(
        input.split(RegExp(r'\s+')).where((emoji) {
          return emoji.trim().isNotEmpty;
        }),
      );
    });
    _emojiController.clear();
  }

  void _removeEmoji(String emoji) {
    if (_emojis.length == 1) return;
    setState(() {
      _emojis.remove(emoji);
    });
  }

  void _addSprite([String? value]) {
    final input = (value ?? _spriteController.text).trim();
    if (input.isEmpty) return;

    setState(() {
      _widgetSprites.add(
        _SpriteBadge(
          label: input,
          icon: Icons.emoji_objects_outlined,
          color:
              Colors.primaries[_widgetSprites.length % Colors.primaries.length],
        ),
      );
    });
    _spriteController.clear();
  }

  void _removeSprite(Widget sprite) {
    if (_widgetSprites.length == 1) return;
    setState(() {
      _widgetSprites.remove(sprite);
    });
  }

  void _showEmojiConfetti() {
    Confetti.show(
      context,
      emojis: _emojis,
      numberOfParticles: 30,
      minimumSize: const Size(24, 24),
      maximumSize: const Size(46, 46),
      gravity: 0.25,
    );
  }

  void _showSpriteConfetti() {
    Confetti.show(
      context,
      widgetSprites: _widgetSprites,
      numberOfParticles: 24,
      minimumSize: const Size(28, 28),
      maximumSize: const Size(54, 54),
      gravity: 0.18,
    );
  }

  void _launchPartyPreset(List<Party> parties) {
    Confetti.launchParties(context, parties);
  }

  Path _drawStar(Size size) {
    double degToRad(double deg) => deg * (pi / 180.0);

    const numberOfPoints = 5;
    final halfWidth = size.width / 2;
    final externalRadius = halfWidth;
    final internalRadius = halfWidth / 2.5;
    final degreesPerStep = degToRad(360 / numberOfPoints);
    final halfDegreesPerStep = degreesPerStep / 2;
    final path = Path();
    final fullAngle = degToRad(360);
    path.moveTo(size.width, halfWidth);

    for (double step = 0; step < fullAngle; step += degreesPerStep) {
      path.lineTo(
        halfWidth + externalRadius * cos(step),
        halfWidth + externalRadius * sin(step),
      );
      path.lineTo(
        halfWidth + internalRadius * cos(step + halfDegreesPerStep),
        halfWidth + internalRadius * sin(step + halfDegreesPerStep),
      );
    }
    path.close();
    return path;
  }

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 4,
      child: Scaffold(
        backgroundColor: const Color(0xFF0F1217),
        appBar: AppBar(
          backgroundColor: const Color(0xFF151922),
          title: const Text('Confetti Plus Examples'),
          bottom: const TabBar(
            isScrollable: true,
            tabs: [
              Tab(icon: Icon(Icons.tune), text: 'Controllers'),
              Tab(icon: Icon(Icons.festival), text: 'Parties'),
              Tab(icon: Icon(Icons.widgets), text: 'Sprites'),
              Tab(icon: Icon(Icons.celebration), text: 'Emojis'),
            ],
          ),
        ),
        body: Stack(
          children: [
            _buildConfettiLayers(),
            SafeArea(
              top: false,
              child: TabBarView(
                children: [
                  _ControllerTab(
                    presets: [
                      _ConfettiPreset(
                        label: 'Blast stars',
                        icon: Icons.auto_awesome,
                        onPressed: _centerController.play,
                        details: const [
                          'blastDirectionality: explosive',
                          'shouldLoop: true',
                          'createParticlePath: drawStar',
                        ],
                      ),
                      _ConfettiPreset(
                        label: 'Left pump',
                        icon: Icons.keyboard_arrow_left,
                        onPressed: _rightController.play,
                        details: const [
                          'alignment: centerRight',
                          'blastDirection: pi',
                          'particleDrag: 0.05',
                        ],
                      ),
                      _ConfettiPreset(
                        label: 'Singles',
                        icon: Icons.looks_one,
                        onPressed: _leftController.play,
                        details: const [
                          'numberOfParticles: 1',
                          'emissionFrequency: 0.6',
                          'maximumSize: Size(50, 50)',
                        ],
                      ),
                      _ConfettiPreset(
                        label: 'Goliath',
                        icon: Icons.south,
                        onPressed: _topController.play,
                        details: const [
                          'alignment: topCenter',
                          'numberOfParticles: 50',
                          'gravity: 1',
                        ],
                      ),
                      _ConfettiPreset(
                        label: 'Cannon',
                        icon: Icons.north,
                        onPressed: _bottomController.play,
                        details: const [
                          'alignment: bottomCenter',
                          'blastDirection: -pi / 2',
                          'maxBlastForce: 100',
                        ],
                      ),
                    ],
                  ),
                  _PartyTab(onLaunch: _launchPartyPreset),
                  _SpriteTab(
                    sprites: _widgetSprites,
                    controller: _spriteController,
                    onAdd: _addSprite,
                    onRemove: _removeSprite,
                    onShow: _showSpriteConfetti,
                  ),
                  _EmojiTab(
                    emojis: _emojis,
                    controller: _emojiController,
                    onAdd: _addEmojis,
                    onRemove: _removeEmoji,
                    onShow: _showEmojiConfetti,
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildConfettiLayers() {
    return IgnorePointer(
      child: Stack(
        children: [
          Align(
            alignment: Alignment.center,
            child: ConfettiWidget(
              confettiController: _centerController,
              blastDirectionality: ConfettiBlastDirection.explosive,
              shouldLoop: true,
              colors: const [
                Colors.green,
                Colors.blue,
                Colors.pink,
                Colors.orange,
                Colors.purple,
              ],
              createParticlePath: _drawStar,
            ),
          ),
          Align(
            alignment: Alignment.centerRight,
            child: ConfettiWidget(
              confettiController: _rightController,
              blastDirection: pi,
              particleDrag: 0.05,
              emissionFrequency: 0.05,
              numberOfParticles: 20,
              gravity: 0.05,
              colors: const [Colors.green, Colors.blue, Colors.pink],
              strokeWidth: 1,
              strokeColor: Colors.white,
            ),
          ),
          Align(
            alignment: Alignment.centerLeft,
            child: ConfettiWidget(
              confettiController: _leftController,
              blastDirection: 0,
              emissionFrequency: 0.6,
              minimumSize: const Size(10, 10),
              maximumSize: const Size(50, 50),
              numberOfParticles: 1,
              gravity: 0.1,
            ),
          ),
          Align(
            alignment: Alignment.topCenter,
            child: ConfettiWidget(
              confettiController: _topController,
              blastDirection: pi / 2,
              maxBlastForce: 5,
              minBlastForce: 2,
              emissionFrequency: 0.05,
              numberOfParticles: 50,
              gravity: 1,
            ),
          ),
          Align(
            alignment: Alignment.bottomCenter,
            child: ConfettiWidget(
              confettiController: _bottomController,
              blastDirection: -pi / 2,
              emissionFrequency: 0.01,
              numberOfParticles: 20,
              maxBlastForce: 100,
              minBlastForce: 80,
              gravity: 0.3,
            ),
          ),
        ],
      ),
    );
  }
}

class _ControllerTab extends StatelessWidget {
  const _ControllerTab({required this.presets});

  final List<_ConfettiPreset> presets;

  @override
  Widget build(BuildContext context) {
    return _TabScaffold(
      title: 'Controller patterns',
      subtitle: 'Long-running emitters driven by ConfettiController.',
      child: LayoutBuilder(
        builder: (context, constraints) {
          final wide = constraints.maxWidth >= 760;
          return GridView.builder(
            shrinkWrap: true,
            physics: const NeverScrollableScrollPhysics(),
            itemCount: presets.length,
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: wide ? 2 : 1,
              mainAxisExtent: 220,
              mainAxisSpacing: 12,
              crossAxisSpacing: 12,
            ),
            itemBuilder: (context, index) {
              return _PresetCard(preset: presets[index]);
            },
          );
        },
      ),
    );
  }
}

class _PartyTab extends StatelessWidget {
  const _PartyTab({required this.onLaunch});

  final ValueChanged<List<Party>> onLaunch;

  @override
  Widget build(BuildContext context) {
    return _TabScaffold(
      title: 'Party presets',
      subtitle: 'One-shot overlay launches using Party configuration.',
      child: Wrap(
        spacing: 12,
        runSpacing: 12,
        children: [
          _PresetCard(
            preset: _ConfettiPreset(
              label: 'Festive',
              icon: Icons.celebration,
              onPressed: () => onLaunch(PartyPresets.festive()),
              details: const [
                'PartyPresets.festive()',
                'Balanced spread',
                'Good default celebration',
              ],
            ),
          ),
          _PresetCard(
            preset: _ConfettiPreset(
              label: 'Explode',
              icon: Icons.bubble_chart,
              onPressed: () => onLaunch(PartyPresets.explode()),
              details: const [
                'PartyPresets.explode()',
                'Center burst',
                'High impact feedback',
              ],
            ),
          ),
          _PresetCard(
            preset: _ConfettiPreset(
              label: 'Parade',
              icon: Icons.directions_run,
              onPressed: () => onLaunch(PartyPresets.parade()),
              details: const [
                'PartyPresets.parade()',
                'Side-to-side movement',
                'Multiple emitters',
              ],
            ),
          ),
          _PresetCard(
            preset: _ConfettiPreset(
              label: 'Rain',
              icon: Icons.water_drop,
              onPressed: () => onLaunch(PartyPresets.rain()),
              details: const [
                'PartyPresets.rain()',
                'Top emitter',
                'Soft falling motion',
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class _SpriteTab extends StatelessWidget {
  const _SpriteTab({
    required this.sprites,
    required this.controller,
    required this.onAdd,
    required this.onRemove,
    required this.onShow,
  });

  final List<Widget> sprites;
  final TextEditingController controller;
  final ValueChanged<String?> onAdd;
  final ValueChanged<Widget> onRemove;
  final VoidCallback onShow;

  @override
  Widget build(BuildContext context) {
    return _TabScaffold(
      title: 'Widget sprites',
      subtitle: 'Any widget can become a confetti particle.',
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          _DemoSurface(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const _PanelTitle(
                  icon: Icons.widgets,
                  title: 'Sprite set',
                  subtitle: 'Add labels, remove previews, then launch.',
                ),
                const SizedBox(height: 14),
                Wrap(
                  spacing: 10,
                  runSpacing: 10,
                  children: [
                    for (final sprite in sprites)
                      _SpritePreview(
                        sprite: sprite,
                        onRemove: () => onRemove(sprite),
                      ),
                  ],
                ),
                const SizedBox(height: 16),
                Row(
                  children: [
                    Expanded(
                      child: TextField(
                        controller: controller,
                        onSubmitted: onAdd,
                        style: const TextStyle(
                          color: Colors.white,
                          fontSize: 16,
                        ),
                        decoration: const InputDecoration(
                          hintText: 'Add sprite label',
                          prefixIcon: Icon(Icons.add_box_outlined),
                          filled: true,
                        ),
                      ),
                    ),
                    const SizedBox(width: 10),
                    IconButton.filledTonal(
                      onPressed: () => onAdd(null),
                      icon: const Icon(Icons.add),
                      tooltip: 'Add sprite',
                    ),
                  ],
                ),
                const SizedBox(height: 16),
                SizedBox(
                  width: double.infinity,
                  child: FilledButton.icon(
                    onPressed: onShow,
                    icon: const Icon(Icons.play_arrow),
                    label: const Text('Launch widget sprites'),
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(height: 12),
          const _CodeCard(
            lines: [
              'Confetti.show(context,',
              '  widgetSprites: sprites,',
              '  numberOfParticles: 24,',
              '  gravity: 0.18,',
              ');',
            ],
          ),
        ],
      ),
    );
  }
}

class _EmojiTab extends StatelessWidget {
  const _EmojiTab({
    required this.emojis,
    required this.controller,
    required this.onAdd,
    required this.onRemove,
    required this.onShow,
  });

  final List<String> emojis;
  final TextEditingController controller;
  final ValueChanged<String?> onAdd;
  final ValueChanged<String> onRemove;
  final VoidCallback onShow;

  @override
  Widget build(BuildContext context) {
    return _TabScaffold(
      title: 'Emoji overlay',
      subtitle: 'Fast bursts without placing a ConfettiWidget in the tree.',
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          _DemoSurface(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const _PanelTitle(
                  icon: Icons.celebration,
                  title: 'Emoji mix',
                  subtitle: 'Enter one or more emoji separated by spaces.',
                ),
                const SizedBox(height: 14),
                Wrap(
                  spacing: 8,
                  runSpacing: 8,
                  children: [
                    for (final emoji in emojis)
                      InputChip(
                        label: Text(
                          emoji,
                          style: const TextStyle(fontSize: 18),
                        ),
                        backgroundColor: Colors.white10,
                        deleteIconColor: Colors.white70,
                        side: const BorderSide(color: Colors.white24),
                        onDeleted:
                            emojis.length == 1 ? null : () => onRemove(emoji),
                      ),
                  ],
                ),
                const SizedBox(height: 14),
                Row(
                  children: [
                    Expanded(
                      child: TextField(
                        controller: controller,
                        onSubmitted: onAdd,
                        style: const TextStyle(
                          color: Colors.white,
                          fontSize: 18,
                        ),
                        decoration: const InputDecoration(
                          hintText: 'Add emoji',
                          prefixIcon: Icon(Icons.add_reaction_outlined),
                          filled: true,
                        ),
                      ),
                    ),
                    const SizedBox(width: 10),
                    IconButton.filledTonal(
                      onPressed: () => onAdd(null),
                      icon: const Icon(Icons.add),
                      tooltip: 'Add emoji',
                    ),
                  ],
                ),
                const SizedBox(height: 16),
                SizedBox(
                  width: double.infinity,
                  child: FilledButton.icon(
                    onPressed: onShow,
                    icon: const Icon(Icons.play_arrow),
                    label: const Text('Launch emoji confetti'),
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(height: 12),
          const _CodeCard(
            lines: [
              'Confetti.show(context,',
              '  emojis: emojis,',
              '  numberOfParticles: 30,',
              '  gravity: 0.25,',
              ');',
            ],
          ),
        ],
      ),
    );
  }
}

class _TabScaffold extends StatelessWidget {
  const _TabScaffold({
    required this.title,
    required this.subtitle,
    required this.child,
  });

  final String title;
  final String subtitle;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.all(18),
      children: [
        _DemoHeader(title: title, subtitle: subtitle),
        const SizedBox(height: 16),
        child,
      ],
    );
  }
}

class _DemoHeader extends StatelessWidget {
  const _DemoHeader({required this.title, required this.subtitle});

  final String title;
  final String subtitle;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          title,
          style: const TextStyle(
            color: Colors.white,
            fontSize: 28,
            fontWeight: FontWeight.w700,
          ),
        ),
        const SizedBox(height: 6),
        Text(
          subtitle,
          style: const TextStyle(color: Colors.white70, fontSize: 15),
        ),
      ],
    );
  }
}

class _PresetCard extends StatelessWidget {
  const _PresetCard({required this.preset});

  final _ConfettiPreset preset;

  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
      constraints: const BoxConstraints(minWidth: 260, maxWidth: 390),
      child: _DemoSurface(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            Row(
              children: [
                Icon(preset.icon, color: Theme.of(context).colorScheme.primary),
                const SizedBox(width: 10),
                Expanded(
                  child: Text(
                    preset.label,
                    style: const TextStyle(
                      color: Colors.white,
                      fontSize: 18,
                      fontWeight: FontWeight.w700,
                    ),
                  ),
                ),
                FilledButton.tonalIcon(
                  onPressed: preset.onPressed,
                  icon: const Icon(Icons.play_arrow),
                  label: const Text('Run'),
                ),
              ],
            ),
            const SizedBox(height: 14),
            _ParameterList(lines: preset.details),
          ],
        ),
      ),
    );
  }
}

class _ParameterList extends StatelessWidget {
  const _ParameterList({required this.lines});

  final List<String> lines;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        for (final line in lines)
          Padding(
            padding: const EdgeInsets.only(bottom: 6),
            child: Row(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Icon(Icons.check, color: Color(0xFF34D399), size: 16),
                const SizedBox(width: 8),
                Expanded(
                  child: Text(
                    line,
                    style: const TextStyle(
                      color: Colors.white70,
                      fontFamily: 'monospace',
                      fontSize: 13,
                    ),
                  ),
                ),
              ],
            ),
          ),
      ],
    );
  }
}

class _CodeCard extends StatelessWidget {
  const _CodeCard({required this.lines});

  final List<String> lines;

  @override
  Widget build(BuildContext context) {
    return _DemoSurface(
      child: SelectableText(
        lines.join('\n'),
        style: const TextStyle(
          color: Color(0xFFD1FAE5),
          fontFamily: 'monospace',
          fontSize: 13,
          height: 1.4,
        ),
      ),
    );
  }
}

class _SpritePreview extends StatelessWidget {
  const _SpritePreview({required this.sprite, required this.onRemove});

  final Widget sprite;
  final VoidCallback onRemove;

  @override
  Widget build(BuildContext context) {
    return Stack(
      clipBehavior: Clip.none,
      children: [
        sprite,
        Positioned(
          right: -6,
          top: -6,
          child: IconButton.filledTonal(
            onPressed: onRemove,
            icon: const Icon(Icons.close, size: 16),
            tooltip: 'Remove sprite',
            visualDensity: VisualDensity.compact,
          ),
        ),
      ],
    );
  }
}

class _SpriteBadge extends StatelessWidget {
  const _SpriteBadge({
    required this.label,
    required this.icon,
    required this.color,
  });

  final String label;
  final IconData icon;
  final Color color;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 88,
      height: 88,
      decoration: BoxDecoration(
        color: color.withValues(alpha: 0.18),
        borderRadius: BorderRadius.circular(8),
        border: Border.all(color: color.withValues(alpha: 0.55)),
      ),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(icon, color: color, size: 30),
          const SizedBox(height: 8),
          Text(
            label,
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
            style: const TextStyle(
              color: Colors.white,
              fontSize: 14,
              fontWeight: FontWeight.w700,
            ),
          ),
        ],
      ),
    );
  }
}

class _DemoSurface extends StatelessWidget {
  const _DemoSurface({required this.child});

  final Widget child;

  @override
  Widget build(BuildContext context) {
    return DecoratedBox(
      decoration: BoxDecoration(
        color: const Color(0xFF1B1E27).withValues(alpha: 0.94),
        border: Border.all(color: Colors.white12),
        borderRadius: BorderRadius.circular(8),
      ),
      child: Padding(padding: const EdgeInsets.all(16), child: child),
    );
  }
}

class _PanelTitle extends StatelessWidget {
  const _PanelTitle({
    required this.icon,
    required this.title,
    required this.subtitle,
  });

  final IconData icon;
  final String title;
  final String subtitle;

  @override
  Widget build(BuildContext context) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Icon(icon, color: Colors.white),
        const SizedBox(width: 10),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                title,
                style: const TextStyle(
                  color: Colors.white,
                  fontSize: 18,
                  fontWeight: FontWeight.w700,
                ),
              ),
              const SizedBox(height: 2),
              Text(
                subtitle,
                style: const TextStyle(color: Colors.white60, fontSize: 13),
              ),
            ],
          ),
        ),
      ],
    );
  }
}

class _ConfettiPreset {
  const _ConfettiPreset({
    required this.label,
    required this.icon,
    required this.onPressed,
    required this.details,
  });

  final String label;
  final IconData icon;
  final VoidCallback onPressed;
  final List<String> details;
}
12
likes
160
points
270
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Flutter confetti package with quick overlays, controller-based widgets, emoji particles, widget sprites, and Konfetti-style party presets.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, vector_math

More

Packages that depend on flutter_confetti_plus