Skip to main content

Catalog · Content

Embed

A poster and a play button in place of a player from another site, which loads only when the visitor asks, after consent.

Fits best: any site — a page that shows a video or a map from another site

Example

NASA’s video of John Young’s salute on the Moon, with a poster of this site. Nothing is requested from the video service until you press the button and agree: the example asks first, as a site that wants consent would, and only then loads the player. Its answer is stored under its own key, luz-consent-demo, so it does not count as an answer for this site.

Play video: John Young’s Lunar Salute on Apollo 16

Without JavaScript the poster is a link to the video.

This is the one page of this site that may load another site, and only after you press the button and agree: the security policy of the site lets this page, and no other, frame www.youtube-nocookie.com.

Known issues

Two things to know before you rely on it.

  • The question has no button to take “Always play” back. To let a visitor withdraw it, offer the category as a checkbox in the cookie banner, with its “change” button: the banner’s answer is merged into the same record, and its luz:consent event lists what was withdrawn in detail.withdrawn.
  • It has not been tested with a screen reader.

Code

Copy the markup

Bundler
import "@luz-ds/catalog/components/embed.css";
HTML
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@luz-ds/catalog@0.3.0/dist/components/embed.min.css" integrity="sha384-4EY9C7PN373Q38ElCXt3Rp+L44k42GwbC9m/nXfz5dvXn0Zeki8AXTPkFLJU3Qrn" crossorigin="anonymous">

The link is the page without JavaScript, so write it as the page you want a reader to land on: it points to the video where it lives. src is the address of the player, in full, as the other site gives it (here, the privacy-enhanced embed address that YouTube gives for a video); LUZ builds no address and names no provider. The poster is your own image, on your own server: a poster fetched from the other site would reach it on page load.

catalog.min.js loads the element’s own file, embed.min.js, when it scans the page; a page without catalog.min.js loads that file on its own.

HTML
<script type="module" src="https://cdn.jsdelivr.net/npm/@luz-ds/catalog@0.3.0/dist/catalog.min.js"></script>

<luz-embed src="https://www.youtube-nocookie.com/embed/g5aPoRtF2vw?autoplay=1" consent="youtube">
  <a class="luz-embed__link" href="https://www.youtube.com/watch?v=g5aPoRtF2vw">
    <img class="luz-embed__poster" src="/media/apollo-16-poster.jpg" alt="" width="1280" height="720" loading="lazy">
    <span class="u-sr-only">Play video: </span>
    <span class="luz-embed__title">John Young’s Lunar Salute on Apollo 16</span>
  </a>
  <div class="luz-embed__ask" data-luz-embed-ask hidden>
    <p>This video is hosted by YouTube (Google). If you play it, YouTube receives your IP address and details of your browser, and may store information on your device. We receive none of it. <a href="/docs/privacy/">Privacy and storage</a></p>
    <div class="o-actions">
      <button class="luz-btn luz-btn--primary" type="button" value="once">Play this video</button>
      <button class="luz-btn luz-btn--primary" type="button" value="cancel">Do not play</button>
      <button class="luz-btn luz-btn--secondary" type="button" value="always">Always play YouTube videos on this site</button>
    </div>
  </div>
</luz-embed>

Your scripts can ask what the element would load:

JS
import { playerURL } from "@luz-ds/catalog/embed";

// The address the element would load, or null when it stays a link
const url = playerURL(document.querySelector("luz-embed"));

Variants and options

There are no variants. The ratio is a property, and the hooks are attributes.

Class or attributeEffect
<luz-embed>
The custom element the module defines: a block with the ratio of the frame. It wires itself when it connects
src
Required. The full address of the player, written by you: https:, or of the page’s own origin. Anything else (javascript:, http: of another origin, a typo) leaves the link
consent
The name of a category of the cookie consent (a lowercase letter, then lowercase letters, digits or hyphens, 32 at most). Until it is granted nothing is requested from the player’s origin, and a click asks first. Without an ask block and without the category granted, it stays the link
data-luz-consent-key
The storage key of the choice, which must start with luz-; luz-consent, the banner’s, by default
data-luz-consent-days, data-luz-consent-policy
Read from the cookie banner of the page, if there is one: how many days “Always” lasts (180 by default, 395 at most) and the version of the policy shown, stored with the choice
--luz-embed-ratio
The ratio of the frame, 16 / 9 by default. Set it on the element: style="--luz-embed-ratio: 4 / 3"
.luz-embed__link
The link of the page without JavaScript: it holds the poster and the title, and takes the frame whole. With JavaScript it becomes a button.luz-embed__play with the same children, so its name does not change
.luz-embed__poster, .luz-embed__title
Your image, which covers the frame, and the title, on a scrim at its foot
.luz-embed__ask with data-luz-embed-ask
Optional, hidden in the markup: the question that shows when the click needs a consent that is not there. Its buttons have a value: once, always or cancel
data-luz-state
Written by the script: ready, ask or playing. Style it, never set it
playerURL(el)
Exported by @luz-ds/catalog/embed: the validated address of the element, or null

How it works

CSS from @luz-ds/catalog and a file of its own, @luz-ds/catalog/embed (dist/embed.min.js, under 1.5 kB compressed), which defines <luz-embed>. It uses no library, and the player’s own script, if any, runs only inside the frame, once loaded. A native video or audio does not need it: use the player. This is for what only another site can show.

  • Before the click: the page holds a poster and a button, and requests nothing from the other site. When the pointer goes over the button, or focus reaches it, the browser is told to open a connection to the player’s origin (a preconnect, once per origin), unless the site wants consent and it is not granted.
  • The click: if nothing needs consent, or it is granted, the frame is written with autoplay permission, and focus goes into it. If consent is needed, the question shows in its place and focus goes to its first button.
  • The question: value="once" plays this video. value="always" plays it and stores the category, as the cookie banner does, in localStorage as {"version":1,"expires":…,"asked":[…],"granted":[…],"given":"…"}, merged into the record that is there so the banner’s categories stay, dispatches luz:consent on document and unlocks every other embed with that category. value="cancel" goes back to the poster, with focus on its button. Escape does nothing; the button that cancels is the way back.
  • The frame: it has the title of the video, allow for the features players use, allowfullscreen and referrerpolicy="strict-origin-when-cross-origin", which some players need to accept the embed.
  • The script writes no text: every word is in your markup, in the page’s language.

A server that builds your pages can write src and save the poster when the content is saved, so a reader never contacts the other site before choosing to.

Without JavaScript

The poster is a link with a visible title that opens the video where it lives, and the question stays hidden. It does the same when src is not valid, and when the category is not granted and there is no question to ask.

Keyboard and ARIA

  • Keys: Tab reaches the button; Enter or Space presses it, as on any button. In the question, Tab goes through its buttons. Escape does nothing.
  • Name: the button has the name of the link it replaces: the hidden words “Play video:” then the title.
  • Focus: to the first button of the question when it shows, into the frame when the player loads, back to the button on Do not play. Once the player is in the frame, its own keys and controls are the other site’s, and the page cannot reach them.
  • Language: the question and the names are yours, in the page’s language. The player’s are the other site’s.

Contrast and focus are in the Design tab.

Design

When to use

Use it for a video, a map or any player that a page takes from another site. Loading such a frame sends the reader’s address to that site, and its script weighs more than the rest of the page; a poster and a button cost one image.

When not to use

Do not use it for a video or sound you host: that is the player, which has controls that LUZ styles and a keyboard that LUZ tests.

  • Do not use it for a link to a page that happens to have a video: that is a card or a link.
  • Do not use it to hide a frame the reader never asked for: it loads only after a click.

It replaces the iframe code that the other site gives you, and the facade scripts for one provider. Nothing in LUZ is removed.

Do and don't

Do Name what happens in each answer: say who hosts the video and what it learns if you play it, and give “Play” and “Do not play” the same weight.
Don't Ask with “OK” and “Cancel”: nobody can tell whether OK loads the video, or agrees to something else.

Writing the content

The title is the title of the video, as the reader would say it, and the hidden words before it say what the button does: “Play video:”. Do not repeat the title in the poster’s alt, which stays empty because the title already names it.

  • Name the service in the question (“hosted by YouTube (Google)”) and say what it learns (“your IP address and details of your browser”) and what it may do (“store information on your device”), in a few plain sentences, with a link to your privacy page.
  • Write the buttons as the action and the thing: “Play this video”, “Do not play”, “Always play YouTube videos on this site”. Give the first two the same weight. Never “OK” or “Yes”.
  • Choose a poster whose important part is in the middle: the play mark covers it.

Contrast and focus

The title rides on a scrim that is solid where the text is, so it reads on a white poster too.

PartColour on backgroundLightDarkHigh contrastNeeds
Title on its scrim
--luz-text-on-fill on --luz-scrim
5.2:1
20.1:1
5.0:1
4.5:1
Triangle of the play mark
--luz-text-on-fill on --luz-brand-fill
6.0:1
4.9:1
14.1:1
3:1
Disc of the play mark, on the lightest poster
--luz-brand-fill on --luz-surface-page
5.6:1
3.5:1
14.1:1
3:1
Edge of the disc, on the darkest poster
--luz-text-on-fill on --luz-surface-media
21.0:1
21.0:1
21.0:1
3:1
Question text
--luz-text-primary on --luz-surface-muted
15.8:1
15.1:1
20.1:1
4.5:1
Focus ring, on the page
--luz-focus-ring on --luz-surface-page
5.6:1
7.7:1
21.0:1
3:1

Measured from the token values of each mode, rounded down. Needs is the WCAG 2.2 AA minimum: 4.5:1 for text, 3:1 for the play mark, focus rings and edges. The scrim is measured over the page, the lightest poster there is.

  • Focus: a 2 px --luz-focus-ring outline around the whole frame, outside it with a 2 px gap, so the poster never hides it. The buttons of the question are core’s.
  • The play mark is a disc and a triangle, not colour alone. In forced colors it is a ring and a triangle in the system’s button text.

Tokens it uses

The 14 tokens that the rules of catalog’s components/embed.css read, by family; the buttons of the question are core’s. Each family links to its page, with the values in every mode. In Figma they are variables with the same names: see Tokens in Figma.

Spacing and sizing
--luz-space-lg, --luz-space-md, --luz-space-sm
Typography
--luz-font-sans, --luz-text-md
Radius
--luz-radius-md

Keyboard and ARIA are in the Code tab.