Skip to content

Track Vimeo Player SDK

Vimeo has no GTM built-in trigger, so every install needs a short bridge: load the Player SDK, wrap each iframe, and push dataLayer events for the lifecycle you care about. This recipe is the drop-in version, with metadata extraction and per-player state.

Valid as of April 2026, Vimeo Player SDK (player.js).

Vimeo’s own analytics is solid for content teams but doesn’t flow into GA4. Without a bridge, Vimeo plays show up in GA4 as generic iframe engagement — no title, no percent, no completion signal. The bridge gives you parity with YouTube in the same video_engagement event family.

Inject Vimeo’s player.js if it isn’t already present, then for each iframe[src*="vimeo.com"] instantiate new Vimeo.Player(iframe) and bind listeners. Fetch title, URL, and duration once via Promise.all so every subsequent event has metadata ready.

dataLayer.push() video_engagement

One Custom HTML tag handles SDK loading, per-iframe wrapping, and all event pushes. Metadata is fetched once and cached per player.

(function() {
// Load SDK if not already present
function ensureSDK(cb) {
if (window.Vimeo && window.Vimeo.Player) return cb();
var s = document.createElement('script');
s.src = 'https://player.vimeo.com/api/player.js';
s.onload = cb;
document.head.appendChild(s);
}
ensureSDK(function() {
var iframes = document.querySelectorAll('iframe[src*="vimeo.com"]');
iframes.forEach(function(iframe) {
var player = new Vimeo.Player(iframe);
var meta = { title: null, url: iframe.src, duration: 0 };
var milestones = [25, 50, 75, 90, 100];
var fired = {};
// Fetch metadata once. Safe to call before 'loaded' fires — the SDK queues.
Promise.all([
player.getVideoTitle(),
player.getVideoUrl().catch(function() { return iframe.src; }),
player.getDuration()
]).then(function(values) {
meta.title = values[0];
meta.url = values[1];
meta.duration = Math.round(values[2]);
});
function push(action, percent, currentTime) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'video_engagement',
video_provider: 'vimeo',
video_action: action,
video_title: meta.title,
video_url: meta.url,
video_duration: meta.duration,
video_current_time: currentTime != null ? Math.round(currentTime) : undefined,
video_percent: percent != null ? percent : 0
});
}
player.on('play', function(data) {
if (!fired.start) {
fired.start = true;
push('start', 0, data && data.seconds);
} else {
push('resume', Math.round((data && data.percent || 0) * 100), data && data.seconds);
}
});
player.on('pause', function(data) {
push('pause', Math.round((data && data.percent || 0) * 100), data && data.seconds);
});
player.on('ended', function(data) {
push('complete', 100, data && data.seconds);
});
player.on('timeupdate', function(data) {
var percent = Math.round((data && data.percent || 0) * 100);
milestones.forEach(function(m) {
if (percent >= m && !fired[m]) {
fired[m] = true;
push('progress_' + m, m, data && data.seconds);
}
});
});
});
});
})();
  1. Add the script as a Custom HTML tag

    • Trigger: Window Loaded — iframes must exist before the querySelectorAll
    • If Vimeo iframes are lazy-loaded, trigger on your custom “video_mounted” event instead
  2. Create a Custom Event trigger

    • Trigger type: Custom Event
    • Event name: video_engagement
  3. Create Data Layer Variables

    • DLV - video_providervideo_provider
    • DLV - video_actionvideo_action
    • DLV - video_titlevideo_title
    • DLV - video_urlvideo_url
    • DLV - video_percentvideo_percent
    • DLV - video_durationvideo_duration
  4. Create a GA4 Event Tag

    • Event name: video_engagement
    • Parameters: all DLVs above, plus page_path{{Page Path}}
    • Trigger: the Custom Event trigger
  5. Filter by video_provider in GA4 reports to separate Vimeo, YouTube, Wistia, and HTML5 plays.

Tag Configuration

GA4 - video_engagement (Vimeo)

Type
Google Analytics: GA4 Event
Trigger
Custom Event - video_engagement
Variables
DLV - video_providerDLV - video_actionDLV - video_titleDLV - video_percent
  1. Open GTM Preview on a page with a Vimeo embed
  2. Press Play — confirm video_engagement with video_action: start, non-empty video_title, and a numeric video_duration
  3. Pause at around 40% — confirm video_action: pause with video_percent near 40
  4. Resume and let playback cross 25, 50, 75, 90, 100 — confirm each progress_* fires once
  5. In GA4 DebugView, verify video_provider: vimeo on every event

SDK load race condition. If another tag on the page already loads player.js, the ensureSDK check prevents a double load. If a third-party script injects a partial window.Vimeo without .Player, the check still passes incorrectly — harden the guard to window.Vimeo && typeof window.Vimeo.Player === 'function' on sites with known conflicts.

Private / domain-restricted videos. player.getVideoTitle() and player.getVideoUrl() reject on private videos when the embedding domain isn’t whitelisted. The .catch on getVideoUrl falls back to the iframe’s own src, which is usually enough. If getVideoTitle() rejects, meta.title stays null — GA4 will record the event with an empty title rather than failing.

Metadata Promise resolves after first event. The play listener can fire before the Promise.all resolves, so the very first video_engagement may have video_title: null. That’s acceptable on most sites; if you need it always populated, push a buffered event from inside .then() or use the 'loaded' event as your start signal.

Multiple players per page. Each player gets its own closure, meta, milestones, and fired map — no shared state. Don’t hoist fired out of the loop or you’ll suppress milestones on the second player.

Autoplay muted counts as start. Same as YouTube — set a user-gesture flag if you need to distinguish intentional plays from autoplay.

timeupdate is chatty. It fires many times per second. The fired map ensures each milestone pushes once. Don’t replace the milestone pattern with a raw timeupdate → push loop or you’ll flood GA4.