Track Video Engagement
Video engagement tracking tells you whether users actually watch your content, not just whether they land on the page. This recipe covers three scenarios: YouTube via GTM’s built-in trigger, Vimeo via the Player API, and HTML5 <video> elements via custom events.
YouTube — GTM Built-in Trigger
Section titled “YouTube — GTM Built-in Trigger”GTM has a YouTube Video trigger that handles progress milestones, play, pause, seek, and completion automatically. It uses the YouTube iFrame Player API under the hood.
-
Enable YouTube Video Trigger
In GTM → Variables → Configure, enable these built-in video variables:
- Video Provider
- Video Status
- Video URL
- Video Title
- Video Duration
- Video Current Time
- Video Percent
-
Create a YouTube Video Trigger
- Trigger type: YouTube Video
- Capture: check Start, Complete, Pause, and Progress
- Add percentages:
10, 25, 50, 75, 90 - Fire on: All Videos (or filter by URL)
-
Create a GA4 Event Tag
- Tag type: Google Analytics: GA4 Event
- Event name:
video_{{Video Status}}— this creates dynamic event names likevideo_start,video_pause,video_complete
Or use a fixed event name
video_engagementwith avideo_actionparameter:video_action→{{Video Status}}video_title→{{Video Title}}video_url→{{Video URL}}video_percent→{{Video Percent}}video_current_time→{{Video Current Time}}video_duration→{{Video Duration}}- Trigger: the YouTube trigger above
-
Test in Preview Mode
Visit a page with an embedded YouTube video. Press Play. The Summary pane should show a
gtm.videoevent within 1-2 seconds.
GA4 - video_engagement
- Type
- Google Analytics: GA4 Event
- Trigger
- YouTube Video - start, pause, progress, complete
- Variables
-
Video StatusVideo TitleVideo URLVideo PercentVideo Duration
Vimeo — Player API
Section titled “Vimeo — Player API”GTM has no built-in Vimeo trigger. You need to load the Vimeo Player SDK and push to the dataLayer from JavaScript.
Push this for each Vimeo player event. Load the Vimeo Player SDK first.
// Load Vimeo Player SDK (or add via a GTM Custom HTML tag)// <script src="https://player.vimeo.com/api/player.js"></script>
document.querySelectorAll('iframe[src*="vimeo.com"]').forEach(function(iframe) { var player = new Vimeo.Player(iframe); var videoData = {};
// Fetch metadata once Promise.all([ player.getVideoTitle(), player.getDuration() ]).then(function(values) { videoData.title = values[0]; videoData.duration = values[1]; videoData.url = iframe.src; });
function pushVideoEvent(action, percent) { window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'video_engagement', video_provider: 'vimeo', video_action: action, video_title: videoData.title, video_url: videoData.url, video_duration: videoData.duration, video_percent: percent || 0 }); }
player.on('play', function() { pushVideoEvent('start', 0); }); player.on('pause', function(data) { pushVideoEvent('pause', Math.round(data.percent * 100)); }); player.on('ended', function() { pushVideoEvent('complete', 100); });
// Progress milestones var milestones = [25, 50, 75, 90]; var fired = {}; player.on('timeupdate', function(data) { var percent = Math.round(data.percent * 100); milestones.forEach(function(m) { if (percent >= m && !fired[m]) { fired[m] = true; pushVideoEvent('progress_' + m, m); } }); });});Add this as a Custom HTML tag in GTM with a Window Loaded trigger so the SDK and iframes are ready.
HTML5 video elements
Section titled “HTML5 video elements”Attach to native HTML5 video elements on your page.
document.querySelectorAll('video').forEach(function(video) { var milestones = [25, 50, 75, 90]; var fired = {}; var videoTitle = video.dataset.title || video.title || video.src.split('/').pop();
function pushVideoEvent(action, percent) { window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'video_engagement', video_provider: 'html5', video_action: action, video_title: videoTitle, video_url: video.currentSrc || video.src, video_duration: Math.round(video.duration), video_percent: percent }); }
video.addEventListener('play', function() { if (!fired.start) { fired.start = true; pushVideoEvent('start', 0); } });
video.addEventListener('pause', function() { if (!video.ended) { pushVideoEvent('pause', Math.round((video.currentTime / video.duration) * 100)); } });
video.addEventListener('ended', function() { pushVideoEvent('complete', 100); });
video.addEventListener('timeupdate', function() { var pct = Math.round((video.currentTime / video.duration) * 100); milestones.forEach(function(m) { if (pct >= m && !fired[m]) { fired[m] = true; pushVideoEvent('progress_' + m, m); } }); });});-
Add the dataLayer push code as a Custom HTML tag in GTM with a Window Loaded trigger. This ensures the video elements exist before you query them.
-
Create a Custom Event Trigger
- Trigger type: Custom Event
- Event name:
video_engagement
-
Create Data Layer Variables
DLV - video_action→video_actionDLV - video_title→video_titleDLV - video_percent→video_percentDLV - video_provider→video_provider
-
Create a GA4 Event Tag
- Event name:
video_engagement - Parameters: map all DLV variables above
- Trigger: the Custom Event trigger
- Event name:
-
Test in Preview Mode
Play the video, pause it, and let it reach 25% completion. Each action should appear as a separate
video_engagementevent in the Summary pane.
Test it
Section titled “Test it”- Open GTM Preview and navigate to a page with a video
- Press Play — verify a
video_startorvideo_engagementevent fires - Let the video reach 25% — verify a progress milestone event fires
- Check the Variables tab for correct
video_title,video_percent, andvideo_duration - In GA4 DebugView, verify events appear with correct parameters
Common gotchas
Section titled “Common gotchas”YouTube trigger requires enablejsapi=1 in the embed URL. GTM’s built-in trigger adds this automatically if it is missing, but consent-blocked iframes may not reload with the parameter. Check Preview mode if no events appear.
Vimeo SDK must load before the iframe. If the SDK script loads after the Vimeo iframes, new Vimeo.Player(iframe) will fail silently. Use a Window Loaded trigger for the Custom HTML tag or add SDK loading to the same tag.
Video loaded dynamically after page load. If your video player is injected after page load (common in SPAs), the document.querySelectorAll call at Window Loaded will find nothing. Use a MutationObserver or push to the dataLayer from your video component’s mount lifecycle instead.