Embed Slides, YouTube Videos, and More

There are plenty of use cases for embedding third-party content on a site, as well as local content that may not be in HTML. Perhaps you gave a talk and want to share your slides. Sometimes you want to reference a video that exists only on YouTube. Maybe you have a functional demo that you want to include alongside the explanation.

Ok, these are my use cases. But they may apply to yours.

The General Technique

Embed a named iframe with a link to load the file or third-party resource. Ideally with some kind of poster image to give a visual cue to what the user will be loading. The content preceding the iframe should give sufficient context for a user who does not see (or load) that visual cue.

We won’t load any external content into the iframe yet thanks to the srcdoc attribute, barring possibly the poster image. The value of the srcdoc attribute is your inlined placeholder web page — or, rather, it is the escaped version of what would live in the <body> of a web page. Including a <style> block.

Here is a minimal example:

<p>
Get <a href="/thing">that thing</a>, ROFL file, 3.3MB
</p>
<iframe
   title="That thing"
   srcdoc="&lt;a href='/thing'&gt;View Thing&lt;/a&gt;"
   >
</iframe>

Use the allow attribute to signal the browser that you want certain features of the embedded content to be supported (gyroscope, geolocation, etc.). If you want to let external content go full-screen, then use the allowfullscreen boolean attribute.

The sandbox attribute is meant to lock down the embedded content. This should result in better security for users but will require you to add tokens for things you want the embedded content to be able to do, such as allow-forms or allow-presentation.

Going into detail for all scenarios and possible attributes is outside the scope of this post, but I encourage you to start with the HTML spec info I listed above. When that proves to be incomprehensible, head over to MDN for all <iframe> attributes. Understand that the MDN support chart has gaps, so you will need to do some testing.

For YouTube

I ripped off this overall approach. In 2019 Arthur Corenzan wrote Lazy load embedded YouTube videos to deal with the half megabyte of tracking and script that YouTube pushes to users just for embedding the video, whether or not they play it. Remy Sharp noted an IE risk, but that has been mooted by the inexorable march of time. I tweaked it and started using it within a month.

Which is why I am sharing my YouTube version first.

Without allowfullscreen, the browser won’t let the user make the YouTube video take up the entire screen. Meanwhile, allow="autoplay" doesn’t seem to have an effect, but I add it to be safe. Be sure the YouTube URL has the ?autoplay=1 query string, but understand that is no guarantee it will actually autoplay when the user activates the link.

Be certain to add a title and give it a value that makes sense within the context of the page. Add the video URL to the src attribute as a fallback. I also add an inline style to set the aspect ratio and make the width 100%. I sometimes remove the width and height attributes YouTube provides (which will be a problem on CSS Naked Day, of course), ideally making it less of a screen-filling block on smaller viewports.

I use the youtube-nocookie.com domain. YouTube offers this domain for its embed code when you Enable privacy-enhanced mode. In addition to supposedly performing less tracking, it is has the advantage of not being blocked by some corporate firewalls that otherwise block YouTube.

While you are grabbing that address from the YouTube embed code, grab the string following the /embed/ up to the ?. That is the unique video identifier (slug). You will run a search and replace against the code I provide to make the embed, fallback source, and poster image all work (though you could use a local poster image for greater privacy for your users). I have highlighted the three places that unique identifier appears in my sample code.

<iframe
   title="Overlays Underwhelm for A11y NYC, Mar 1 2022"
   style="aspect-ratio: 16 / 9; width:100%;"
   src="https://www.youtube-nocookie.com/embed/Jfn8LFc-eGA"
   allow="autoplay"
   allowfullscreen
   loading="lazy"
   srcdoc="&lt;style&gt;body{background-image:url(https://i3.ytimg.com/vi/Jfn8LFc-eGA/hqdefault.jpg);background-repeat:no-repeat;background-size:cover;background-position:center center;display:grid;place-items:center;min-height:97dvh;overflow:hidden;}a{display:block;width:96px;height:96px;overflow:hidden;}a:focus{outline:none;}a:focus circle,a:hover circle{fill:#000;}a:focus circle:first-child + circle,a:hover circle:first-child + circle{stroke-dasharray:.4,.4;}a:focus polygon,a:hover polygon{stroke:#fff;stroke-width:.75;}&lt;/style&gt;&lt;a href='https://www.youtube-nocookie.com/embed/Jfn8LFc-eGA?autoplay=1'&gt;&lt;svg viewBox='0 0 16 16' width='96' height='96' xmlns='http://www.w3.org/2000/svg' aria-hidden='true'&gt;&lt;circle cx='50%' cy='50%' r='7.75' fill='none' stroke='#000' stroke-width='.5'/&gt;&lt;circle cx='50%' cy='50%' r='7.25' fill='none' stroke='#fff' stroke-width='.5'/&gt;&lt;circle cx='50%' cy='50%' r='7' fill='#0009'/&gt;&lt;polygon points='12, 8 6, 4.5 6, 11.5' fill='#fff' stroke-linejoin='round'&gt;&lt;/polygon&gt;&lt;/svg&gt;Play&lt;/a&gt;"></iframe>

The bulk of that code is the SVG play button and its focus & hover styles. You can make something simpler if you wish.

For Code Demos

Sometimes, as a developer, you want to have a demo in your page. Dropping the demo directly into the page can conflict with the styles already there, so an iframe makes sense.

In most cases, the approach I offer here may be overkill. However, if that demo does novel stuff, such as stealing focus, animating content, playing audio or video, firing up giant script libraries, and so on, you may want to give the user the option to load it only when they are ready.

This is simpler than the YouTube embed, but you will want to adjust the allow attribute for your use case. Unlike a video’s play button, users don’t necessarily expect a symbol in this context so you can get away with a text link.

I like to include a poster image to give the user some context what they might load, but I want to keep the file size small and, in the case of the example here, blur it so the reader doesn’t accidentally think it is the real demo. Again, you should choose what to do based on your audience and context.

I also set max-height to 90vh because I don’t know how someone will be surfing (zoomed, on a tiny display, landscape on a mobile, etc.) and don’t want my demo to take over their entire viewport, which can mess with scrolling (and grokking). As above, consider your audience and the context.

<iframe
   title="Focus Stealing Example"
   width="100%"
   height="400"
   style="width:100%;height:400px;max-height:90vh;"
   src="https://adrianroselli.com/demos/2.5.8-adversarial-conformance/"
   loading="lazy"
   srcdoc="&lt;style&gt;*{padding:0;margin:0;overflow:hidden}body{font-family:'Segoe UI',-apple-system, BlinkMacSystemFont,Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif;line-height:1.4;background:transparent url('https://adrianroselli.com/wp-content/uploads/2023/12/2.5.8-modal_poster.jpg') no-repeat center;background-size:cover}p{display:grid;grid-template-columns:repeat(auto-fit, 60vw);grid-template-rows:repeat(auto-fit,100vh);justify-content:center}a{align-self:center;background:rgba(0,0,0,0.75);font-weight:bold;color:#fff;font-size:7.5vw;line-height:1;text-align:center;padding:15vh 10vw}a:hover,a:focus{background:#000;outline:.2rem solid #ff0;outline-offset:.3rem;}&lt;/style&gt;&lt;p&gt;&lt;a href='https://adrianroselli.com/demos/2.5.8-adversarial-conformance/'&gt;Show Demo&lt;/a&gt;&lt;/p&gt;"></iframe>

The bulk of that code is the CSS of the placeholder page. The following embedded example steals focus, so after you activate the link give it a 5 count before you do anything else (because it will steal the focus).

This can work with sites like CodePen, even though they already provide their own embedding tools. In the scenario where you don’t want to make users run third-party scripts when loading your page and instead let them choose when to do it, adapt the iframe. Granted, you may lose some of the benefits of the embedded version.

With CodePen, you can choose which code editor to show (I show HTML), where to display the editor (I put it on the left), and even pull the poster image from the CodePen site (assuming you are fine with loading from an external site). The CodePen site documents how to set each. I have highlighted the “slug” in the code sample, which search and replace can handle for you.

<iframe
   title="2.5.8 Target Size examples"
   width="100%"
   height="550"
   style="width:100%;height:550px;max-height:90vh;"
   src="https://codepen.io/aardrian/pen/OJBZyjL"
   loading="lazy"
   srcdoc="&lt;style&gt;*{padding:0;margin:0;overflow:hidden}body{font-family:'Segoe UI',-apple-system, BlinkMacSystemFont,Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif;line-height:1.4;background:transparent url('https://codepen.io/aardrian/pen/OJBZyjL/image/large.png') no-repeat center;background-size:cover}p{display:grid;grid-template-columns:repeat(auto-fit, 60vw);grid-template-rows:repeat(auto-fit,100vh);justify-content:center}a{align-self:center;background:rgba(0,0,0,0.75);font-weight:bold;color:#fff;font-size:7.5vw;line-height:1;text-align:center;padding:15vh 10vw}a:hover,a:focus{background:#000;outline:.2rem solid #ff0;outline-offset:.3rem;}&lt;/style&gt;&lt;p&gt;&lt;a href='https://codepen.io/aardrian/pen/OJBZyjL?layout=left&amp;editors=1000'&gt;Load 2.5.8 Examples&lt;/a&gt;&lt;/p&gt;"></iframe>

Which results in this (and which also steals focus):

Added the next day: Last month Álvaro Montoro created a CodePen option that starts as a link and is progressively enhanced to become the CodePen embed in Creating Progressive Enhanced CodePen Links and Embeds. Completely different approach than mine. I leave it to you, dear reader, to evaluate each approach on your own (or your employer’s) time.

For Slide Decks

For years I used SlideShare to embed slides into posts. I opted to do this versus sharing the PowerPoint file because I have had my slides taken and re-used without my permission. The problem, of course, is that SlideShare has stacks of accessibility barriers. This is compounded by SlideShare deciding to share your original PowerPoint for all decks, forcing you to disable it for every deck individually (no, there was no forewarning).

I tried Notist, but found it impossible to use with just a keyboard or with a screen reader (even with well-formed decks). At that point I decided I was better off hosting the decks on my own site.

I use a PDF generated from my PowerPoint (and embed any videos from the slides into the same post). For this to work, you have to export accessible PDFs, which means you start with accessible PowerPoints.

Granted, the PDF viewer in your browser may not expose those accessibility features, so ensuring your users can also download the PDF is critical. Bear in mind that some users may have configured their browsers to never view a PDF and to download it instead. Offering the link is especially important since you don’t want to push a massive file to them on page load.

Ensure allowfullscreen is in there, especially if you share your slides during a talk so folks can more easily follow along on their mobile. The poster image I use is generally the intro slide for the presentation.

<iframe
   title="WCAGmire slides for Paris Web 2023 by Adrian Roselli"
   width="100%"
   height="550"
   style="width:100%;height:550px;max-height:90vh;aspect-ratio: 16/9;"
   src="https://adrianroselli.com/wp-content/uploads/2023/09/AdrianRoselli_ParisWeb.pdf"
   allowfullscreen
   loading="lazy"
   srcdoc="&lt;style&gt;*{padding:0;margin:0;overflow:hidden}body{font-family:'Segoe UI',-apple-system, BlinkMacSystemFont,Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif;line-height:1.4;background:transparent url('https://adrianroselli.com/wp-content/uploads/2023/09/AdrianRoselli_ParisWeb_poster.jpg') no-repeat center;background-size:cover}p{display:grid;grid-template-columns:repeat(auto-fit, 60vw);grid-template-rows:repeat(auto-fit,100vh);justify-content:center}a{align-self:center;background:rgba(0,0,0,0.75);font-weight:bold;color:#fff;font-size:7.5vw;text-align:center;padding:15vh 10vw;outline-offset:0;outline-color:#fff;border-radius:1em;}a:focus,a:hover{background-color:#000;outline:.1em solid;outline-offset:.1em;outline-color:#ff0;transition:all .15s ease-out;box-shadow:0 0 20vw #000;}&lt;/style&gt;&lt;p&gt;&lt;a href='https://adrianroselli.com/wp-content/uploads/2023/09/AdrianRoselli_ParisWeb.pdf'&gt;Display PDF&lt;/a&gt;&lt;/p&gt;"></iframe>

As before, the bulk of that code is due to custom styles.

You could conceivably use this approach for any PDF (or other file that you know the browser will render). For example, to embed frivolous SLAPP lawsuits provided as untagged PDFs (with a height set to make it much taller on the page):

Can These Be Web Components?

Yes. But only because web components got cool in the last few months. Also people have figured out they can make them without using shadow DOM. All of the previous examples could be converted to web components, but when you do so you need to ensure they fail gracefully.

I made a crappy web component for the YouTube embed to demonstrate. You should look at adapting this to the HTML that works best for you. And probably fixing my code. After encircling your computer in a six-foot salt ring. Not iodized.

The critical part of this code is that the custom element needs to work on its own even if the JavaScript fails. I use a link. After the script runs, the link is retained so other factors that may prevent the iframe from working won’t prevent the user from accessing the content.

The web component relies on the youtu.be domain for the link to pull the video slug. It uses the text of the link to populate the iframe’s name. The entirety of the text within the custom element is retained (yes, you can easily break it; don’t). If you leave off data-aspectratio, it defaults to 16/9.

<youtube-embed data-aspectratio="16/9">
  <p>
    YouTube: <a href="https://youtu.be/kSQ8tZ35t4U">They Might Be Giants, “When Will You Die?”</a>, 2:42
  </p>
</youtube-embed>

If the script fails for whatever reason, users get this:

YouTube: They Might Be Giants, “When Will You Die?”, 2:42

I highlight where the video slug gets defined and used. Like I said, sub-par JavaScript:

class YouTubeEmbed extends HTMLElement {
  constructor() {
    super();
  }
  connectedCallback() {
    var iframe = document.createElement('iframe');
    var theLink = this.querySelector('a');
    var vidSlug = theLink.getAttribute('href').split('/')[3];
    iframe.setAttribute('title',theLink.textContent);
    iframe.setAttribute('allow','autoplay');
    iframe.setAttribute('allowfullscreen','');
    iframe.setAttribute('loading','lazy');
    iframe.setAttribute('src','https://www.youtube-nocookie.com/embed/'+vidSlug);
    if (this.getAttribute('data-target')) {
      iframe.setAttribute('style','aspect-ratio:'+this.data-aspectratio('data-aspectratio')+';width:100%;');
    } else {
      iframe.setAttribute('style','aspect-ratio:16/9;width:100%;');
    }
    iframe.setAttribute('srcdoc','<style>body{background-image:url(https://i3.ytimg.com/vi/'+vidSlug+'/hqdefault.jpg);background-repeat:no-repeat;background-size:cover;background-position:center center;display:grid;place-items:center;min-height:97dvh;overflow:hidden;}a{display:block;width:96px;height:96px;overflow:hidden;}a:focus{outline:none;}a:focus circle,a:hover circle{fill:#000;}a:focus circle:first-child + circle,a:hover circle:first-child + circle{stroke-dasharray:.4,.4;}a:focus polygon,a:hover polygon{stroke:#fff;stroke-width:.75;}</style><a href="https://www.youtube-nocookie.com/embed/'+vidSlug+'?autoplay=1"><svg viewBox="0 0 16 16" width="96" height="96" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><circle cx="50%" cy="50%" r="7.75" fill="none" stroke="#000" stroke-width=".5"/><circle cx="50%" cy="50%" r="7.25" fill="none" stroke="#fff" stroke-width=".5"/><circle cx="50%" cy="50%" r="7" fill="#0009"/><polygon points="12, 8 6, 4.5 6, 11.5" fill="#fff" stroke-linejoin="round"></polygon></svg>Play</a>');
    this.appendChild(iframe);
  }
}
customElements.define('youtube-embed', YouTubeEmbed);

When embedded and the script runs, we get this:

YouTube: They Might Be Giants, “When Will You Die?”, 2:42

I made one for Vimeo, but it requires a call to the Vimeo API to get the thumbnail image. I also made a web component that accepts both Vimeo and YouTube, but yeah, it’s a bit embarrassing so y’all will have to imagine it instead.

Bonus!

Print your own dayglo pink paper monster truck hearse with this untagged PDF (796kb)! This is not a web component. At least not without a V8 engine.

Wrap-up

You will want to adapt each of these to your own use case, site styles, and user expectations. I wouldn’t consider them copy-paste-ready, but I have put some effort into the accessibility of each approach.

You may have noticed that my link, focus, and hover styles are all over the place. This is partly to show you some variety. It’s also partly because there are different ways you can make sure they have sufficient contrast and visual cues so that you don’t run afoul of WCAG.

Remember to set the placeholder page background color in the srcdoc or it will take your page’s background (not always ideal). Also, be sure to test yours in forced colors mode (Windows High Contrast Mode / Contrast Themes).

Remember to escape URLs. Remember to offer a text link. Remember to note the format and file size. Remember to check all those nifty <iframe> attributes and test them across your audience’s browsers.

If you have fixes, suggestions, alternate approaches, and so on, add them in the comments. These are by no means the only way to pull off embedded content.

2 Comments

Dan Tripp; . Permalink
In response to Dan Tripp. Reply

Indeed it was. Thanks!

Leave a Comment or Response

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>