Skip to content
SlashClone

Embed YouTube videos without tracking cookies

@slashclone/privacy-friendly-youtube-embed

Embed YouTube videos on your site without loading anything from Google until the visitor presses Play or allows videos. A click-to-play placeholder with a local poster swaps in the youtube-nocookie.com player, with a keyboard-friendly button, no layout shift, CSP entries, and a Playwright test that proves no YouTube request happens first. React and HTML templates. Not legal advice.

SkillDevOpsv0.1.0
Activity
New: be the first to try it
  1. 1// Click-to-play YouTube embeds for plain HTML. No dependencies, no build step.
  2. 2//
  3. 3// Markup (see youtube-embed.html):
  4. 4// <div class="yt-embed" data-video="VIDEO_ID_OR_LINK" data-title="Title"
  5. 5// data-duration="4:12" data-poster="/posters/intro.jpg"></div>
  6. 6//
  7. 7// Nothing is fetched from Google until the visitor presses Play, or until your
  8. 8// consent layer calls YouTubeEmbeds.setAllowed(true) because the visitor allowed
  9. 9// embedded videos. To offer "always allow videos" here, set
  10. 10// YouTubeEmbeds.onAllowAll = function () { ... } before calling init().
  11. 12(function () {
  12. 13 "use strict";
  13. 15 var PLAYER_ORIGIN = "https://www.youtube-nocookie.com";
  14. 16 var ID = /^[A-Za-z0-9_-]{11}$/;
  15. 17 var allowed = false;
  16. 19 function parseVideoId(input) {
  17. 20 if (ID.test(input)) return input;
  18. 21 var u;
  19. 22 try {
  20. 23 u = new URL(input);
  21. 24 } catch (e) {
  22. 25 return null;
  23. 26 }
  24. 27 if (u.protocol !== "https:" && u.protocol !== "http:") return null;
  25. 28 var host = u.hostname.toLowerCase().replace(/^(www|m|music)\./, "");
  26. 29 var id = null;
  27. 30 if (host === "youtu.be") id = u.pathname.split("/")[1] || null;
  28. 31 else if (host === "youtube.com" || host === "youtube-nocookie.com") {
  29. 32 if (u.pathname === "/watch") id = u.searchParams.get("v");
  30. 33 else {
  31. 34 var m = /^\/(?:embed|shorts|live|v)\/([^/?#]+)/.exec(u.pathname);
  32. 35 id = m ? m[1] : null;
  33. 36 }
  34. 37 }
  35. 38 return id && ID.test(id) ? id : null;
  36. 39 }
  37. 41 function el(tag, attrs, text) {
  38. 42 var n = document.createElement(tag);
  39. 43 for (var k in attrs) n.setAttribute(k, attrs[k]);
  40. 44 if (text) n.textContent = text;
  41. 45 return n;
  42. 46 }
  43. 48 function playerSrc(id, box, autoplay) {
  44. 49 var p = new URLSearchParams({ rel: "0", playsinline: "1" });
  45. 50 if (autoplay) p.set("autoplay", "1");
  46. 51 var start = parseInt(box.dataset.start || "", 10);
  47. 52 if (start > 0) p.set("start", String(start));
  48. 53 if (box.dataset.captions === "true") p.set("cc_load_policy", "1");
  49. 54 return PLAYER_ORIGIN + "/embed/" + id + "?" + p.toString();
  50. 55 }
  51. 57 function showPlayer(box, clicked) {
  52. 58 if (box.dataset.state === "player") return;
  53. 59 var id = box.dataset.videoId;
  54. 60 var frame = el("iframe", {
  55. 61 src: playerSrc(id, box, clicked),
  56. 62 title: box.dataset.title || "YouTube video",
  57. 63 loading: clicked ? "eager" : "lazy",
  58. 64 allow: "autoplay; encrypted-media; picture-in-picture; fullscreen",
  59. 65 allowfullscreen: "",
  60. 66 referrerpolicy: "strict-origin-when-cross-origin",
  61. 67 });
  62. 68 frame.className = "yt-embed__fill";
  63. 69 box.replaceChildren(frame);
  64. 70 box.dataset.state = "player";
  65. 71 if (clicked) frame.focus();
  66. 72 }
  67. 74 function showPlaceholder(box) {
  68. 75 var id = box.dataset.videoId;
  69. 76 var title = box.dataset.title || "YouTube video";
  70. 77 box.replaceChildren();
  71. 78 box.dataset.state = "placeholder";
  72. 80 if (box.dataset.poster) {
  73. 81 var img = el("img", { src: box.dataset.poster, alt: "", loading: "lazy", decoding: "async" });
  74. 82 img.className = "yt-embed__fill yt-embed__poster";
  75. 83 box.appendChild(img);
  76. 84 }
  77. 86 var layer = el("div", {});
  78. 87 layer.className = "yt-embed__fill yt-embed__layer";
  79. 89 var heading = el("p", {}, title);
  80. 90 heading.className = "yt-embed__title";
  81. 91 if (box.dataset.duration)
  82. 92 heading.appendChild(el("span", {}, " (" + box.dataset.duration + ")"));
  83. 94 var play = el("button", { type: "button", "aria-label": "Play video: " + title }, "Play video");
  84. 95 play.className = "yt-embed__play";
  85. 96 play.addEventListener("click", function () {
  86. 97 showPlayer(box, true);
  87. 98 });
  88. 100 var note = el("p", {}, "Playing loads the video from YouTube, which may set cookies. ");
  89. 101 note.className = "yt-embed__note";
  90. 102 note.appendChild(
  91. 103 el(
  92. 104 "a",
  93. 105 {
  94. 106 href: "https://www.youtube.com/watch?v=" + id,
  95. 107 rel: "noopener noreferrer",
  96. 108 target: "_blank",
  97. 109 },
  98. 110 "Watch on YouTube",
  99. 111 ),
  100. 112 );
  101. 113 if (typeof api.onAllowAll === "function") {
  102. 114 note.appendChild(document.createTextNode(" or "));
  103. 115 var always = el("button", { type: "button" }, "always allow videos");
  104. 116 always.className = "yt-embed__link";
  105. 117 always.addEventListener("click", function () {
  106. 118 api.onAllowAll();
  107. 119 });
  108. 120 note.appendChild(always);
  109. 121 }
  110. 122 note.appendChild(document.createTextNode("."));
  111. 124 layer.appendChild(heading);
  112. 125 layer.appendChild(play);
  113. 126 layer.appendChild(note);
  114. 127 box.appendChild(layer);
  115. 128 }
  116. 130 function init(root) {
  117. 131 var boxes = (root || document).querySelectorAll(".yt-embed:not([data-state])");
  118. 132 for (var i = 0; i < boxes.length; i++) {
  119. 133 var box = boxes[i];
  120. 134 var id = parseVideoId(box.dataset.video || "");
  121. 135 if (!id) {
  122. 136 box.hidden = true;
  123. 137 continue;
  124. 138 }
  125. 139 box.dataset.videoId = id;
  126. 140 if (allowed) showPlayer(box, false);
  127. 141 else showPlaceholder(box);
  128. 142 }
  129. 143 }
  130. 145 // Call with true when the visitor grants embedded videos, false when they revoke.
  131. 146 // Revoking replaces players with placeholders; cookies YouTube already set are
  132. 147 // the consent layer's job to explain or clear.
  133. 148 function setAllowed(value) {
  134. 149 allowed = !!value;
  135. 150 var boxes = document.querySelectorAll(".yt-embed[data-video-id]");
  136. 151 for (var i = 0; i < boxes.length; i++) {
  137. 152 if (allowed) showPlayer(boxes[i], false);
  138. 153 else showPlaceholder(boxes[i]);
  139. 154 }
  140. 155 }
  141. 157 var api = { init: init, setAllowed: setAllowed, parseVideoId: parseVideoId, onAllowAll: null };
  142. 158 window.YouTubeEmbeds = api;
  143. 159})();

Cookies that keep you signed in and remember these choices are always on. Everything else is off until you turn it on.