diff --git a/CHANGELOG.md b/CHANGELOG.md
index ddf2c3c4eb972815937606ef1a3886246790948b..4335441de397c5ee6fcf13cc9cc54f17f73ba05a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Added `Tweenable::is_looping()`, `Tweenable::set_progress()`, `Tweenable::times_completed()`, and `Tweenable::rewind()`.
 - Added `Animator::set_progress()`, `Animator::progress()`, `Animator::stop()`, and `Animator::rewind()`.
 - Added `AssetAnimator::set_progress()`, `AssetAnimator::progress()`, `AssetAnimator::stop()`, and `AssetAnimator::rewind()`.
+- Added the `TweenCompleted` event, raised when a `Tween<T>` completed its animation if that feature was previously activated with `set_completed_event()` or `with_completed_event()`.
 
 ### Changed
 
diff --git a/README.md b/README.md
index 6efebcdc4c728e5183de445d78124b3daff537c0..335ba82bf4993f0f840b6812af80d62f7980bc52 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,7 @@ Tweening animation plugin for the Bevy game engine.
 - [x] Animate any field of any component or asset, including custom ones.
 - [x] Run multiple tweens (animations) per component/asset in parallel.
 - [x] Chain multiple tweens (animations) one after the other for complex animations.
+- [x] Raise a Bevy event or invoke a callback when an tween completed.
 
 ## Usage
 
diff --git a/examples/sequence.rs b/examples/sequence.rs
index 08d7ad7ca727fa7d3cc45e1cf5a2cfa1a14311b1..0cefa6b2abe43ebb9f93d2bf8effbf2ee433341b 100644
--- a/examples/sequence.rs
+++ b/examples/sequence.rs
@@ -1,223 +1,226 @@
-use bevy::prelude::*;
-use bevy_tweening::{lens::*, *};
-use std::time::Duration;
-
-fn main() -> Result<(), Box<dyn std::error::Error>> {
-    App::default()
-        .insert_resource(WindowDescriptor {
-            title: "Sequence".to_string(),
-            width: 600.,
-            height: 600.,
-            vsync: true,
-            ..Default::default()
-        })
-        .add_plugins(DefaultPlugins)
-        .add_plugin(TweeningPlugin)
-        .add_startup_system(setup)
-        .add_system(update_text)
-        .run();
-
-    Ok(())
-}
-
-#[derive(Component)]
-struct RedProgress;
-
-#[derive(Component)]
-struct BlueProgress;
-
-#[derive(Component)]
-struct RedSprite;
-
-#[derive(Component)]
-struct BlueSprite;
-
-fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
-    commands.spawn_bundle(OrthographicCameraBundle::new_2d());
-
-    let font = asset_server.load("fonts/FiraMono-Regular.ttf");
-    let text_style_red = TextStyle {
-        font: font.clone(),
-        font_size: 50.0,
-        color: Color::RED,
-    };
-    let text_style_blue = TextStyle {
-        font: font.clone(),
-        font_size: 50.0,
-        color: Color::BLUE,
-    };
-
-    let text_alignment = TextAlignment {
-        vertical: VerticalAlign::Center,
-        horizontal: HorizontalAlign::Center,
-    };
-
-    // Text with the index of the active tween in the sequence
-    commands
-        .spawn_bundle(Text2dBundle {
-            text: Text {
-                sections: vec![
-                    TextSection {
-                        value: "progress: ".to_owned(),
-                        style: text_style_red.clone(),
-                    },
-                    TextSection {
-                        value: "0%".to_owned(),
-                        style: text_style_red.clone(),
-                    },
-                ],
-                alignment: text_alignment,
-            },
-            transform: Transform::from_translation(Vec3::new(0., 40., 0.)),
-            ..Default::default()
-        })
-        .insert(RedProgress);
-
-    // Text with progress of the active tween in the sequence
-    commands
-        .spawn_bundle(Text2dBundle {
-            text: Text {
-                sections: vec![
-                    TextSection {
-                        value: "progress: ".to_owned(),
-                        style: text_style_blue.clone(),
-                    },
-                    TextSection {
-                        value: "0%".to_owned(),
-                        style: text_style_blue.clone(),
-                    },
-                ],
-                alignment: text_alignment,
-            },
-            transform: Transform::from_translation(Vec3::new(0., -40., 0.)),
-            ..Default::default()
-        })
-        .insert(BlueProgress);
-
-    let size = 25.;
-
-    let margin = 40.;
-    let screen_x = 600.;
-    let screen_y = 600.;
-    let center = Vec3::new(screen_x / 2., screen_y / 2., 0.);
-
-    // Run around the window from corner to corner
-    let dests = &[
-        Vec3::new(margin, margin, 0.),
-        Vec3::new(screen_x - margin, margin, 0.),
-        Vec3::new(screen_x - margin, screen_y - margin, 0.),
-        Vec3::new(margin, screen_y - margin, 0.),
-        Vec3::new(margin, margin, 0.),
-    ];
-    // Build a sequence from an iterator over a Tweenable (here, a Tween<Transform>)
-    let seq = Sequence::new(dests.windows(2).map(|pair| {
-        Tween::new(
-            EaseFunction::QuadraticInOut,
-            TweeningType::Once,
-            Duration::from_secs(1),
-            TransformPositionLens {
-                start: pair[0] - center,
-                end: pair[1] - center,
-            },
-        )
-        .with_completed_event(true) // Get an event after each segment
-    }));
-
-    commands
-        .spawn_bundle(SpriteBundle {
-            sprite: Sprite {
-                color: Color::RED,
-                custom_size: Some(Vec2::new(size, size)),
-                ..Default::default()
-            },
-            ..Default::default()
-        })
-        .insert(RedSprite)
-        .insert(Animator::new(seq));
-
-    // First move from left to right, then rotate around self 180 degrees while scaling
-    // size at the same time.
-    let tween_move = Tween::new(
-        EaseFunction::QuadraticInOut,
-        TweeningType::Once,
-        Duration::from_secs(1),
-        TransformPositionLens {
-            start: Vec3::new(-200., 100., 0.),
-            end: Vec3::new(200., 100., 0.),
-        },
-    )
-    .with_completed_event(true); // Get an event once move completed
-    let tween_rotate = Tween::new(
-        EaseFunction::QuadraticInOut,
-        TweeningType::Once,
-        Duration::from_secs(1),
-        TransformRotationLens {
-            start: Quat::IDENTITY,
-            end: Quat::from_rotation_z(180_f32.to_radians()),
-        },
-    );
-    let tween_scale = Tween::new(
-        EaseFunction::QuadraticInOut,
-        TweeningType::Once,
-        Duration::from_secs(1),
-        TransformScaleLens {
-            start: Vec3::ONE,
-            end: Vec3::splat(2.0),
-        },
-    );
-    // Build parallel tracks executing two tweens at the same time : rotate and scale.
-    let tracks = Tracks::new([tween_rotate, tween_scale]);
-    // Build a sequence from an heterogeneous list of tweenables by casting them manually
-    // to a boxed Tweenable<Transform> : first move, then { rotate + scale }.
-    let seq2 = Sequence::new([
-        Box::new(tween_move) as Box<dyn Tweenable<Transform> + Send + Sync + 'static>,
-        Box::new(tracks) as Box<dyn Tweenable<Transform> + Send + Sync + 'static>,
-    ]);
-
-    commands
-        .spawn_bundle(SpriteBundle {
-            sprite: Sprite {
-                color: Color::BLUE,
-                custom_size: Some(Vec2::new(size * 3., size)),
-                ..Default::default()
-            },
-            ..Default::default()
-        })
-        .insert(BlueSprite)
-        .insert(Animator::new(seq2));
-}
-
-fn update_text(
-    // Note: need a QuerySet<> due to the "&mut Text" in both queries
-    mut query_text: QuerySet<(
-        QueryState<&mut Text, With<RedProgress>>,
-        QueryState<&mut Text, With<BlueProgress>>,
-    )>,
-    query_anim_red: Query<&Animator<Transform>, With<RedSprite>>,
-    query_anim_blue: Query<&Animator<Transform>, With<BlueSprite>>,
-    mut query_event: EventReader<TweenCompleted>,
-) {
-    let anim_red = query_anim_red.single();
-    let tween_red = anim_red.tweenable().unwrap();
-    let progress_red = tween_red.progress();
-
-    let anim_blue = query_anim_blue.single();
-    let tween_blue = anim_blue.tweenable().unwrap();
-    let progress_blue = tween_blue.progress();
-
-    // Use scopes to force-drop the mutable context before opening the next one
-    {
-        let mut q0 = query_text.q0();
-        let mut red_text = q0.single_mut();
-        red_text.sections[1].value = format!("{:5.1}%", progress_red * 100.).to_string();
-    }
-    {
-        let mut q1 = query_text.q1();
-        let mut blue_text = q1.single_mut();
-        blue_text.sections[1].value = format!("{:5.1}%", progress_blue * 100.).to_string();
-    }
-
-    for ev in query_event.iter() {
-        println!("Event: TweenCompleted entity={:?}", ev.entity);
-    }
-}
+use bevy::prelude::*;
+use bevy_tweening::{lens::*, *};
+use std::time::Duration;
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+    App::default()
+        .insert_resource(WindowDescriptor {
+            title: "Sequence".to_string(),
+            width: 600.,
+            height: 600.,
+            vsync: true,
+            ..Default::default()
+        })
+        .add_plugins(DefaultPlugins)
+        .add_plugin(TweeningPlugin)
+        .add_startup_system(setup)
+        .add_system(update_text)
+        .run();
+
+    Ok(())
+}
+
+#[derive(Component)]
+struct RedProgress;
+
+#[derive(Component)]
+struct BlueProgress;
+
+#[derive(Component)]
+struct RedSprite;
+
+#[derive(Component)]
+struct BlueSprite;
+
+fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
+    commands.spawn_bundle(OrthographicCameraBundle::new_2d());
+
+    let font = asset_server.load("fonts/FiraMono-Regular.ttf");
+    let text_style_red = TextStyle {
+        font: font.clone(),
+        font_size: 50.0,
+        color: Color::RED,
+    };
+    let text_style_blue = TextStyle {
+        font: font.clone(),
+        font_size: 50.0,
+        color: Color::BLUE,
+    };
+
+    let text_alignment = TextAlignment {
+        vertical: VerticalAlign::Center,
+        horizontal: HorizontalAlign::Center,
+    };
+
+    // Text with the index of the active tween in the sequence
+    commands
+        .spawn_bundle(Text2dBundle {
+            text: Text {
+                sections: vec![
+                    TextSection {
+                        value: "progress: ".to_owned(),
+                        style: text_style_red.clone(),
+                    },
+                    TextSection {
+                        value: "0%".to_owned(),
+                        style: text_style_red.clone(),
+                    },
+                ],
+                alignment: text_alignment,
+            },
+            transform: Transform::from_translation(Vec3::new(0., 40., 0.)),
+            ..Default::default()
+        })
+        .insert(RedProgress);
+
+    // Text with progress of the active tween in the sequence
+    commands
+        .spawn_bundle(Text2dBundle {
+            text: Text {
+                sections: vec![
+                    TextSection {
+                        value: "progress: ".to_owned(),
+                        style: text_style_blue.clone(),
+                    },
+                    TextSection {
+                        value: "0%".to_owned(),
+                        style: text_style_blue.clone(),
+                    },
+                ],
+                alignment: text_alignment,
+            },
+            transform: Transform::from_translation(Vec3::new(0., -40., 0.)),
+            ..Default::default()
+        })
+        .insert(BlueProgress);
+
+    let size = 25.;
+
+    let margin = 40.;
+    let screen_x = 600.;
+    let screen_y = 600.;
+    let center = Vec3::new(screen_x / 2., screen_y / 2., 0.);
+
+    // Run around the window from corner to corner
+    let dests = &[
+        Vec3::new(margin, margin, 0.),
+        Vec3::new(screen_x - margin, margin, 0.),
+        Vec3::new(screen_x - margin, screen_y - margin, 0.),
+        Vec3::new(margin, screen_y - margin, 0.),
+        Vec3::new(margin, margin, 0.),
+    ];
+    // Build a sequence from an iterator over a Tweenable (here, a Tween<Transform>)
+    let seq = Sequence::new(dests.windows(2).enumerate().map(|(index, pair)| {
+        Tween::new(
+            EaseFunction::QuadraticInOut,
+            TweeningType::Once,
+            Duration::from_secs(1),
+            TransformPositionLens {
+                start: pair[0] - center,
+                end: pair[1] - center,
+            },
+        )
+        .with_completed_event(true, index as u64) // Get an event after each segment
+    }));
+
+    commands
+        .spawn_bundle(SpriteBundle {
+            sprite: Sprite {
+                color: Color::RED,
+                custom_size: Some(Vec2::new(size, size)),
+                ..Default::default()
+            },
+            ..Default::default()
+        })
+        .insert(RedSprite)
+        .insert(Animator::new(seq));
+
+    // First move from left to right, then rotate around self 180 degrees while scaling
+    // size at the same time.
+    let tween_move = Tween::new(
+        EaseFunction::QuadraticInOut,
+        TweeningType::Once,
+        Duration::from_secs(1),
+        TransformPositionLens {
+            start: Vec3::new(-200., 100., 0.),
+            end: Vec3::new(200., 100., 0.),
+        },
+    )
+    .with_completed_event(true, 99); // Get an event once move completed
+    let tween_rotate = Tween::new(
+        EaseFunction::QuadraticInOut,
+        TweeningType::Once,
+        Duration::from_secs(1),
+        TransformRotationLens {
+            start: Quat::IDENTITY,
+            end: Quat::from_rotation_z(180_f32.to_radians()),
+        },
+    );
+    let tween_scale = Tween::new(
+        EaseFunction::QuadraticInOut,
+        TweeningType::Once,
+        Duration::from_secs(1),
+        TransformScaleLens {
+            start: Vec3::ONE,
+            end: Vec3::splat(2.0),
+        },
+    );
+    // Build parallel tracks executing two tweens at the same time : rotate and scale.
+    let tracks = Tracks::new([tween_rotate, tween_scale]);
+    // Build a sequence from an heterogeneous list of tweenables by casting them manually
+    // to a boxed Tweenable<Transform> : first move, then { rotate + scale }.
+    let seq2 = Sequence::new([
+        Box::new(tween_move) as Box<dyn Tweenable<Transform> + Send + Sync + 'static>,
+        Box::new(tracks) as Box<dyn Tweenable<Transform> + Send + Sync + 'static>,
+    ]);
+
+    commands
+        .spawn_bundle(SpriteBundle {
+            sprite: Sprite {
+                color: Color::BLUE,
+                custom_size: Some(Vec2::new(size * 3., size)),
+                ..Default::default()
+            },
+            ..Default::default()
+        })
+        .insert(BlueSprite)
+        .insert(Animator::new(seq2));
+}
+
+fn update_text(
+    // Note: need a QuerySet<> due to the "&mut Text" in both queries
+    mut query_text: QuerySet<(
+        QueryState<&mut Text, With<RedProgress>>,
+        QueryState<&mut Text, With<BlueProgress>>,
+    )>,
+    query_anim_red: Query<&Animator<Transform>, With<RedSprite>>,
+    query_anim_blue: Query<&Animator<Transform>, With<BlueSprite>>,
+    mut query_event: EventReader<TweenCompleted>,
+) {
+    let anim_red = query_anim_red.single();
+    let tween_red = anim_red.tweenable().unwrap();
+    let progress_red = tween_red.progress();
+
+    let anim_blue = query_anim_blue.single();
+    let tween_blue = anim_blue.tweenable().unwrap();
+    let progress_blue = tween_blue.progress();
+
+    // Use scopes to force-drop the mutable context before opening the next one
+    {
+        let mut q0 = query_text.q0();
+        let mut red_text = q0.single_mut();
+        red_text.sections[1].value = format!("{:5.1}%", progress_red * 100.).to_string();
+    }
+    {
+        let mut q1 = query_text.q1();
+        let mut blue_text = q1.single_mut();
+        blue_text.sections[1].value = format!("{:5.1}%", progress_blue * 100.).to_string();
+    }
+
+    for ev in query_event.iter() {
+        println!(
+            "Event: TweenCompleted entity={:?} user_data={}",
+            ev.entity, ev.user_data
+        );
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index e48e947e29158babb6d10120679eccd71e6d2e04..845325914e0802cc9a529f486172b67208fb2aee 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -170,11 +170,13 @@ pub use tweenable::{Delay, Sequence, Tracks, Tween, TweenCompleted, TweenState,
 /// Type of looping for a tween animation.
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum TweeningType {
-    /// Run the animation once from state to end only.
+    /// Run the animation once from start to end only.
     Once,
-    /// Looping, restarting from the start once finished.
+    /// Loop the animation indefinitely, restarting from the start each time the end is reached.
     Loop,
-    /// Repeat the animation back and forth.
+    /// Loop the animation back and forth, changing direction each time an endpoint is reached.
+    /// A complete cycle start -> end -> start always counts as 2 loop iterations for the various
+    /// operations where looping matters.
     PingPong,
 }
 
diff --git a/src/tweenable.rs b/src/tweenable.rs
index 45d71ef801659126a981f8269bda4cba8244ee7f..e5a324773ee56d343bf2d7c8e1366cf4bda3c28b 100644
--- a/src/tweenable.rs
+++ b/src/tweenable.rs
@@ -17,10 +17,29 @@ pub enum TweenState {
 }
 
 /// Event raised when a tween completed.
+///
+/// This event is raised when a tween completed. For non-looping tweens, this is raised once at the
+/// end of the animation. For looping animations, this is raised once per iteration. In case the animation
+/// direction changes ([`TweeningType::PingPong`]), an iteration corresponds to a single progress from
+/// one endpoint to the other, whatever the direction. Therefore a complete cycle start -> end -> start
+/// counts as 2 iterations and raises 2 events (one when reaching the end, one when reaching back the start).
+///
+/// # Note
+///
+/// The semantic is slightly different from [`TweenState::Completed`], which indicates that the tweenable
+/// has finished ticking and do not need to be updated anymore, a state which is never reached for looping
+/// animation. Here the [`TweenCompleted`] event instead marks the end of a single loop iteration.
 #[derive(Copy, Clone)]
 pub struct TweenCompleted {
     /// The [`Entity`] the tween which completed and its animator are attached to.
     pub entity: Entity,
+    /// An opaque value set by the user when activating event raising, used to identify the particular
+    /// tween which raised this event. The value is passed unmodified from a call to [`with_completed_event()`]
+    /// or [`set_completed_event()`].
+    ///
+    /// [`with_completed_event()`]: Tween::with_completed_event
+    /// [`set_completed_event()`]: Tween::set_completed_event
+    pub user_data: u64,
 }
 
 /// An animatable entity, either a single [`Tween`] or a collection of them.
@@ -136,7 +155,7 @@ pub struct Tween<T> {
     times_completed: u32,
     lens: Box<dyn Lens<T> + Send + Sync + 'static>,
     on_completed: Option<Box<dyn Fn(Entity, &Tween<T>) + Send + Sync + 'static>>,
-    raise_event: bool,
+    event_data: Option<u64>,
 }
 
 impl<T: 'static> Tween<T> {
@@ -207,18 +226,43 @@ impl<T> Tween<T> {
             times_completed: 0,
             lens: Box::new(lens),
             on_completed: None,
-            raise_event: false,
+            event_data: None,
         }
     }
 
     /// Enable or disable raising a completed event.
     ///
     /// If enabled, the tween will raise a [`TweenCompleted`] event when the animation completed.
-    /// This is similar to the [`set_completed`] callback, but uses Bevy events instead.
+    /// This is similar to the [`set_completed()`] callback, but uses Bevy events instead.
     ///
-    /// [`set_completed`]: Tween::set_completed
-    pub fn with_completed_event(mut self, enabled: bool) -> Self {
-        self.raise_event = enabled;
+    /// # Example
+    /// ```
+    /// # use bevy_tweening::{lens::*, *};
+    /// # use bevy::{ecs::event::EventReader, math::Vec3};
+    /// # use std::time::Duration;
+    /// let tween = Tween::new(
+    ///     // [...]
+    /// #    EaseFunction::QuadraticInOut,
+    /// #    TweeningType::Once,
+    /// #    Duration::from_secs_f32(1.0),
+    /// #    TransformPositionLens {
+    /// #        start: Vec3::ZERO,
+    /// #        end: Vec3::new(3.5, 0., 0.),
+    /// #    },
+    /// )
+    /// .with_completed_event(true, 42);
+    ///
+    /// fn my_system(mut reader: EventReader<TweenCompleted>) {
+    ///   for ev in reader.iter() {
+    ///     assert_eq!(ev.user_data, 42);
+    ///     println!("Entity {:?} raised TweenCompleted!", ev.entity);
+    ///   }
+    /// }
+    /// ```
+    ///
+    /// [`set_completed()`]: Tween::set_completed
+    pub fn with_completed_event(mut self, enabled: bool, user_data: u64) -> Self {
+        self.event_data = if enabled { Some(user_data) } else { None };
         self
     }
 
@@ -250,11 +294,14 @@ impl<T> Tween<T> {
     /// Enable or disable raising a completed event.
     ///
     /// If enabled, the tween will raise a [`TweenCompleted`] event when the animation completed.
-    /// This is similar to the [`set_completed`] callback, but uses Bevy events instead.
+    /// This is similar to the [`set_completed()`] callback, but uses Bevy events instead.
+    ///
+    /// See [`with_completed_event()`] for details.
     ///
-    /// [`set_completed`]: Tween::set_completed
-    pub fn set_completed_event(&mut self, enabled: bool) {
-        self.raise_event = enabled;
+    /// [`set_completed()`]: Tween::set_completed
+    /// [`with_completed_event()`]: Tween::with_completed_event
+    pub fn set_completed_event(&mut self, enabled: bool, user_data: u64) {
+        self.event_data = if enabled { Some(user_data) } else { None };
     }
 }
 
@@ -316,8 +363,11 @@ impl<T> Tweenable<T> for Tween<T> {
             // Timer::times_finished() returns the number of finished times since last tick only
             self.times_completed += self.timer.times_finished();
 
-            if self.raise_event {
-                event_writer.send(TweenCompleted { entity });
+            if let Some(user_data) = &self.event_data {
+                event_writer.send(TweenCompleted {
+                    entity,
+                    user_data: *user_data,
+                });
             }
             if let Some(cb) = &self.on_completed {
                 cb(entity, &self);