diff --git a/assets/particles/star_06.png b/assets/particles/star_06.png
new file mode 100644
index 0000000..8ff3b13
Binary files /dev/null and b/assets/particles/star_06.png differ
diff --git a/src/battle.rs b/src/battle.rs
index fea7a1a..48e976b 100644
--- a/src/battle.rs
+++ b/src/battle.rs
@@ -7,13 +7,19 @@ use bevy::{
     prelude::*,
     render::{camera::ScalingMode, view::RenderLayers},
 };
+use bevy_tweening::{
+    lens::TransformPositionLens, Animator, Delay, EaseFunction, Tween, TweeningType,
+};
 use iyes_loopless::prelude::*;
 use strum::{EnumCount, IntoEnumIterator};
 use strum_macros::{Display, EnumCount, EnumIter, EnumVariantNames};
 
 use crate::{
-    board::{BoardPrefab, BoardState, Element, Match},
+    board::{
+        BoardPrefab, BoardState, Element, Match, Tile, BETWEEN_MATCH_DELAY, MATCH_START_DELAY,
+    },
     cards::{CardsPrefab, CardsState},
+    particles::ParticleEmitter,
     player::{Player, Spell},
     prefab::{spawn, Prefab},
     transitions::{FadeScreenPrefab, TransitionDirection, TransitionEnd},
@@ -50,6 +56,7 @@ impl Plugin for BattlePlugin {
                 ConditionSet::new()
                     .run_in_state(BattleState::PlayerTurn)
                     .with_system(track_matches)
+                    .with_system(animate_matches)
                     .with_system(start_outtro)
                     .with_system(kill_enemies)
                     .with_system(end_player_turn.run_in_state(BoardState::End))
@@ -57,7 +64,9 @@ impl Plugin for BattlePlugin {
             )
             .add_enter_system(
                 BoardState::End,
-                player_attack.run_in_state(BattleState::PlayerTurn),
+                player_attack
+                    .chain(animate_attack)
+                    .run_in_state(BattleState::PlayerTurn),
             )
             .add_enter_system(BattleState::EnemyTurn, enemies_attack)
             .add_system_set(
@@ -301,6 +310,146 @@ fn track_matches(mut events: EventReader<Match>, mut matches: ResMut<Matches>) {
     matches.0.extend(events.iter().cloned());
 }
 
+fn animate_matches(
+    mut events: EventReader<Match>,
+    mut commands: Commands,
+    tiles: Query<&GlobalTransform, With<Tile>>,
+    mut materials: ResMut<Assets<StandardMaterial>>,
+    asset_server: Res<AssetServer>,
+    player: Res<Player>,
+) {
+    if let Some(spell) = player.active_spell.as_ref() {
+        let start_delay = Duration::from_secs_f32(MATCH_START_DELAY);
+        let delay_between_matches = Duration::from_secs_f32(BETWEEN_MATCH_DELAY);
+
+        let mut delay = start_delay;
+        for event in events
+            .iter()
+            .filter(|x| spell.elements.contains(&x.element))
+        {
+            let material = materials.add(StandardMaterial {
+                base_color: event.element.color(),
+                base_color_texture: Some(asset_server.load("particles/star_06.png")),
+                double_sided: true,
+                unlit: true,
+                alpha_mode: AlphaMode::Blend,
+                ..default()
+            });
+
+            for tile in &event.tiles {
+                let transform = tiles.get(*tile).unwrap();
+
+                let transform = transform.compute_transform() * Transform::from_xyz(0.0, 0.0, 1.0);
+                commands
+                    .spawn_bundle(SpatialBundle {
+                        transform,
+                        ..default()
+                    })
+                    .insert(BOARD_LAYER)
+                    .insert(DelayedDespawn::from_seconds(0.7))
+                    .insert(Animator::new(Delay::new(delay).then(Tween::new(
+                        EaseFunction::QuadraticInOut,
+                        TweeningType::Once,
+                        Duration::from_secs_f32(0.5),
+                        TransformPositionLens {
+                            start: transform.translation,
+                            end: Vec3::new(0.0, -2.5, 1.0),
+                        },
+                    ))))
+                    .with_children(|c| {
+                        c.spawn_bundle(SpatialBundle::default())
+                            .insert(ParticleEmitter {
+                                material: material.clone(),
+                                timer: Timer::from_seconds(1.0 / 200.0, true),
+                                size_range: 0.2..0.3,
+                                velocity_range: -0.01..0.01,
+                                lifetime_range: 0.5..1.0,
+                                particles_track: true,
+                            });
+
+                        c.spawn_bundle(SpatialBundle::default())
+                            .insert(ParticleEmitter {
+                                material: material.clone(),
+                                timer: Timer::from_seconds(1.0 / 100.0, true),
+                                size_range: 0.2..0.3,
+                                velocity_range: -0.01..0.01,
+                                lifetime_range: 0.2..0.5,
+                                particles_track: false,
+                            });
+                    });
+
+                delay += delay_between_matches;
+            }
+        }
+    }
+}
+
+fn animate_attack(
+    matches: Res<Matches>,
+    mut commands: Commands,
+    mut materials: ResMut<Assets<StandardMaterial>>,
+    asset_server: Res<AssetServer>,
+    player: Res<Player>,
+) {
+    if let Some(spell) = player.active_spell.as_ref() {
+        for event in matches
+            .0
+            .iter()
+            .filter(|x| spell.elements.contains(&x.element))
+        {
+            let material = materials.add(StandardMaterial {
+                base_color: event.element.color(),
+                base_color_texture: Some(asset_server.load("particles/star_06.png")),
+                double_sided: true,
+                unlit: true,
+                alpha_mode: AlphaMode::Blend,
+                ..default()
+            });
+
+            for _ in &event.tiles {
+                let transform = Transform::from_xyz(0.0, -2.5, 1.0);
+                commands
+                    .spawn_bundle(SpatialBundle {
+                        transform,
+                        ..default()
+                    })
+                    .insert(BOARD_LAYER)
+                    .insert(DelayedDespawn::from_seconds(0.7))
+                    .insert(Animator::new(Tween::new(
+                        EaseFunction::QuadraticInOut,
+                        TweeningType::Once,
+                        Duration::from_secs_f32(0.5),
+                        TransformPositionLens {
+                            start: transform.translation,
+                            end: Vec3::new(0.0, 2.1, 1.0),
+                        },
+                    )))
+                    .with_children(|c| {
+                        c.spawn_bundle(SpatialBundle::default())
+                            .insert(ParticleEmitter {
+                                material: material.clone(),
+                                timer: Timer::from_seconds(1.0 / 200.0, true),
+                                size_range: 0.2..0.3,
+                                velocity_range: -0.01..0.01,
+                                lifetime_range: 0.5..1.0,
+                                particles_track: true,
+                            });
+
+                        c.spawn_bundle(SpatialBundle::default())
+                            .insert(ParticleEmitter {
+                                material: material.clone(),
+                                timer: Timer::from_seconds(1.0 / 100.0, true),
+                                size_range: 0.2..0.3,
+                                velocity_range: -0.01..0.01,
+                                lifetime_range: 0.2..0.5,
+                                particles_track: false,
+                            });
+                    });
+            }
+        }
+    }
+}
+
 fn player_attack(
     mut enemies: Query<(&mut Enemy, &mut EnemyAnimator, &EnemyAnimations)>,
     mut animation_players: Query<&mut AnimationPlayer>,
diff --git a/src/board.rs b/src/board.rs
index 0367eec..afedeba 100644
--- a/src/board.rs
+++ b/src/board.rs
@@ -378,10 +378,13 @@ fn match_gems(
     events.send_batch(matches.into_iter());
 }
 
+pub const MATCH_START_DELAY: f32 = 0.1;
+pub const BETWEEN_MATCH_DELAY: f32 = 0.1;
+
 fn destroy_matches(mut events: EventReader<Match>, tiles: Query<&Tile>, mut commands: Commands) {
-    let start_delay = Duration::from_secs_f32(0.1);
+    let start_delay = Duration::from_secs_f32(MATCH_START_DELAY);
     let delay_between_gems = Duration::from_secs_f32(0.0);
-    let delay_between_matches = Duration::from_secs_f32(0.2);
+    let delay_between_matches = Duration::from_secs_f32(BETWEEN_MATCH_DELAY);
     let animation_time = Duration::from_secs_f32(0.1);
 
     let mut delay = start_delay;
diff --git a/src/lib.rs b/src/lib.rs
index 05abef9..46c1030 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -5,23 +5,25 @@ use board::{BoardPlugin, BoardState};
 use cards::{CardPlugin, CardsState};
 use iyes_loopless::prelude::*;
 use main_state::{MainState, MainStatePlugin};
+use particles::ParticlesPlugin;
 use std::{fmt::Debug, hash::Hash};
 use transitions::TransitionPlugin;
 use utils::UtilsPlugin;
 
+#[cfg(target_arch = "wasm32")]
+mod wasm;
+
 mod battle;
 mod board;
 mod cards;
 mod main_state;
+mod particles;
 mod player;
 mod prefab;
 mod transitions;
 mod tween_untils;
 mod ui;
-mod utils;
-
-#[cfg(target_arch = "wasm32")]
-mod wasm;
+pub mod utils;
 
 pub fn build_app() -> App {
     let mut app = App::new();
@@ -51,6 +53,7 @@ pub fn build_app() -> App {
     .add_plugin(BattlePlugin)
     .add_plugin(TransitionPlugin)
     .add_plugin(MainStatePlugin)
+    .add_plugin(ParticlesPlugin)
     .add_system(log_states::<BoardState>)
     .add_system(log_states::<BattleState>)
     .add_system(log_states::<MainState>)
diff --git a/src/particles.rs b/src/particles.rs
new file mode 100644
index 0000000..404d307
--- /dev/null
+++ b/src/particles.rs
@@ -0,0 +1,104 @@
+use std::ops::Range;
+
+use bevy::{
+    pbr::{NotShadowCaster, NotShadowReceiver},
+    prelude::*,
+    render::view::RenderLayers,
+};
+
+use crate::utils::square_mesh;
+
+pub struct ParticlesPlugin;
+
+impl Plugin for ParticlesPlugin {
+    fn build(&self, app: &mut App) {
+        app.add_system(emit_particles).add_system(move_particles);
+    }
+}
+
+#[derive(Component)]
+pub struct Particle {
+    pub lifetime: Timer,
+    pub velocity: Vec3,
+}
+
+#[derive(Component)]
+pub struct ParticleEmitter {
+    pub material: Handle<StandardMaterial>,
+    pub timer: Timer,
+    pub size_range: Range<f32>,
+    pub velocity_range: Range<f32>,
+    // in seconds
+    pub lifetime_range: Range<f32>,
+    pub particles_track: bool,
+}
+
+fn emit_particles(
+    mut emitters: Query<(
+        Entity,
+        &mut ParticleEmitter,
+        &GlobalTransform,
+        Option<&RenderLayers>,
+    )>,
+    mut commands: Commands,
+    time: Res<Time>,
+) {
+    let mut rng = fastrand::Rng::new();
+    for (entity, mut emitter, transform, render_layers) in &mut emitters {
+        for _ in 0..(emitter.timer.tick(time.delta()).times_finished_this_tick()) {
+            let lifetime = random_in_range(&emitter.lifetime_range, &mut rng);
+            let size = random_in_range(&emitter.size_range, &mut rng);
+            let velocity = Vec2::new(
+                random_in_range(&emitter.velocity_range, &mut rng),
+                random_in_range(&emitter.velocity_range, &mut rng),
+            )
+            .extend(0.0);
+
+            let particle = commands
+                .spawn_bundle(PbrBundle {
+                    mesh: square_mesh(),
+                    material: emitter.material.clone(),
+                    transform: if emitter.particles_track {
+                        default()
+                    } else {
+                        transform.compute_transform()
+                    }
+                    .with_scale(Vec3::splat(size)),
+                    ..default()
+                })
+                .insert(Particle {
+                    lifetime: Timer::from_seconds(lifetime, false),
+                    velocity,
+                })
+                .insert(NotShadowCaster)
+                .insert(NotShadowReceiver)
+                .id();
+
+            if emitter.particles_track {
+                commands.entity(entity).add_child(particle);
+            }
+
+            if let Some(render_layers) = render_layers {
+                commands.entity(particle).insert(*render_layers);
+            }
+        }
+    }
+}
+
+fn random_in_range(range: &Range<f32>, rng: &mut fastrand::Rng) -> f32 {
+    rng.f32() * (range.end - range.start) + range.start
+}
+
+fn move_particles(
+    mut particles: Query<(Entity, &mut Particle, &mut Transform)>,
+    mut commands: Commands,
+    time: Res<Time>,
+) {
+    for (entity, mut particle, mut transform) in &mut particles {
+        if particle.lifetime.tick(time.delta()).finished() {
+            commands.entity(entity).despawn();
+        } else {
+            transform.translation += particle.velocity;
+        }
+    }
+}
diff --git a/src/utils.rs b/src/utils.rs
index 839d21f..c5bf7d9 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -2,7 +2,7 @@ use std::{hash::Hash, time::Duration};
 
 use bevy::{
     asset::HandleId,
-    ecs::system::AsSystemLabel,
+    ecs::{query::QueryEntityError, system::AsSystemLabel},
     pbr::{NotShadowCaster, NotShadowReceiver},
     prelude::{shape::Quad, *},
     reflect::TypeUuid,
@@ -24,12 +24,20 @@ impl Plugin for UtilsPlugin {
             .add_event::<WorldCursorEvent>()
             .add_startup_system(add_meshes)
             .add_startup_system(add_materials)
-            .add_system(delayed_despawn)
+            .add_stage_before(
+                CoreStage::PostUpdate,
+                "delayed_despawn",
+                SystemStage::parallel(),
+            )
+            .add_system_to_stage("delayed_despawn", delayed_despawn)
             .add_system_to_stage(
                 CoreStage::PostUpdate,
                 update_progress.before(TransformSystem::TransformPropagate),
             )
-            .add_system_to_stage(CoreStage::PostUpdate, propagate_render_layers)
+            .add_system_to_stage(
+                "delayed_despawn",
+                propagate_render_layers.before(delayed_despawn),
+            )
             .add_system_to_stage(CoreStage::PreUpdate, update_world_cursors)
             .add_system_to_stage(
                 CoreStage::PreUpdate,
@@ -153,12 +161,16 @@ fn propagate_render_layers(
 
         while !children.is_empty() {
             for child in std::mem::take(&mut children) {
-                if let Ok(mut child_layer) = layers.get_mut(child) {
-                    if *child_layer != layer {
-                        *child_layer = layer;
+                match layers.get_mut(child) {
+                    Ok(mut child_layer) => {
+                        if *child_layer != layer {
+                            *child_layer = layer;
+                        }
                     }
-                } else {
-                    commands.entity(child).insert(layer);
+                    Err(QueryEntityError::QueryDoesNotMatch(entity)) => {
+                        commands.entity(entity).insert(layer);
+                    }
+                    _ => {}
                 }
 
                 children.extend(children_query.get(child).into_iter().flatten().cloned());