local-iframe
v2.2.1
Published
Web component that allows you to render local code sandboxes using iframes and HTML templates.
Maintainers
Readme
local-iframe
Web component that allows you to render local code sandboxes using iframes and HTML templates.
Table of Contents
Getting Started
You may either install and import the package:
npm install local-iframe// Option 1: simplest, auto-registers as <local-iframe>
import "local-iframe";
// Option 2: import the class, register it yourself, do whatever you want
import { LocalIframe } from "local-iframe/LocalIframe";
window.customElements.define("code-sandbox", LocalIframe);Or include it via CDN as an ES Module:
<script
type="module"
src="https://cdn.jsdelivr.net/npm/[email protected]/+esm"
></script>Then, render it in your document, and define all of the markup for the iframe inside a <template>. Here's a simple counter demo:
<local-iframe>
<template>
<button>Increment</button>
<div>Count: <output>0</output></div>
<script>
const button = document.querySelector("button");
const output = document.querySelector("output");
button.addEventListener("click", () => {
output.innerHTML = parseInt(output.innerHTML, 10) + 1;
});
</script>
<style>
button {
padding: 8px;
background-color: transparent;
}
</style>
</template>
</local-iframe>local-iframe will copy that inner template and render a fully local iframe with this exact HTML, including CSS and JavaScript, producing the following output:
<local-iframe>
<template><!-- omitted for brevity --></template>
<iframe
style="height: 100%; width: 100%; max-width: 100%;"
srcdoc='<!DOCTYPE html><html><head><meta charset="utf-8"><title></title></head><body>
<button>Increment</button>
<div>Count: <output>0</output></div>
<script>
const button = document.querySelector("button");
const output = document.querySelector("output");
button.addEventListener("click", () => {
output.innerHTML = parseInt(output.innerHTML, 10) + 1;
});
</script>
<style>
button {
padding: 8px;
background-color: transparent;
}
</style>
</body></html>'
></iframe
></local-iframe>All styles and scripts will only affect elements within the iframe itself.
See examples and usage patterns.
Use Cases and Motivation
When writing web development tutorials, I sometimes want to allow my readers to interact with code demos or see live output, rather than just sharing screenshots and code samples. Some of the best examples of this are in my article on JavaScript events, where readers can click buttons to see events get logged to the console. All of the demos are fully isolated from each other, which greatly simplifies the content authoring experience for me.
It's easy to do this with a service like Codepen, but I don't like the idea of having to maintain my code demos outside my blog on a third-party website and then embedding them in my Markdown, which often loads a lot of extra JavaScript that I don't need.
Instead, local-iframe uses the HTMLIFrameElement.srcdoc attribute to create a fully self-contained iframe based on whatever code you give it, right there in your page. What you see is what you get.
[!NOTE] This originally began as an Eleventy plugin: eleventy-plugin-code-demo.
[!TIP] See also: "Building HTML, CSS, and JS code preview using iframe's srcdoc attribute" by Maciej Mionskowski, which is what originally inspired me to poke around and see how far I could take this idea.
Examples and Usage Patterns
Rendering the iframe Content
There are two ways to define the markup for your iframe:
- Render a
<template>as a child of<local-iframe>. - Render a
<template>elsewhere in the DOM and reference it by ID.
Option 1: Child <template>
As we've already seen, the simplest way to use local-iframe is to render a <template> as a child:
<local-iframe description="Counter demo">
<template>
<button>Increment</button>
<div>Count: <output>0</output></div>
<script>
const button = document.querySelector("button");
const output = document.querySelector("output");
button.addEventListener("click", () => {
output.innerHTML = parseInt(output.innerHTML, 10) + 1;
});
</script>
<style>
button {
padding: 8px;
background-color: transparent;
}
</style>
</template>
</local-iframe>Option 2: template Attribute
Alternatively, you can define a <template> elsewhere in the DOM and reference it by ID with the template attribute, like so:
<!-- This is what we want to render in the iframe -->
<template id="my-template">
<h1>Template</h1>
<style>
h1 {
color: red;
}
</style>
</template>
<!-- Reference the ID of the template above with the `template` attribute -->
<local-iframe
template="my-template"
description="External template demo"
></local-iframe>If you change the template attribute, the underlying iframe will re-render with the new markup.
Providing a Custom iframe
By default, local-iframe appends an iframe to its subtree if one does not already exist:
<local-iframe>
<template></template>
<!-- The component adds this automatically for you -->
<iframe srcdoc="..."></iframe>
</local-iframe>Optionally, you may provide a custom iframe as a child, and that will get used instead:
<local-iframe>
<template></template>
<!-- We'll use this iframe instead of inserting a new one -->
<iframe style="border: solid 1px red"></iframe>
</local-iframe>This allows you to set custom attributes on the iframe yourself, without having to forward them via a bloated API.
Advanced: Overriding iframe.srcdoc
By default, LocalIframe renders an inner iframe whose srcdoc is the following barebones HTML document:
_render(templateHtml) {
return `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>${this.description}</title>
</head>
<body>${templateHtml}</body>
</html>`;
}However, if you want to render a custom document that will be shared by all iframes, you can:
- Subclass
LocalIframe, - Override the
_rendermethod, and - Register it as a new custom element.
Like this:
import { LocalIframe } from "local-iframe/LocalIframe";
class CodeDemo extends LocalIframe {
_render(templateHtml) {
return `<!DOCTYPE html>
<html>
<head><meta charset="utf-8">
<title>${this.description}</title>
<style>body { background-color: red }</style>
</head>
<body>
${templateHtml}
<p>Brought to you by CodeDemo</p>
</body></html>`;
}
}
window.customElements.define("code-demo", CodeDemo);As expected, your custom class will inherit all public/protected properties, attributes, and methods from LocalIframe.
This way, you don't need to duplicate the shared markup across all of your <template>s.
You can also create as many custom variants as you want using this pattern.
Minimize Layout Shifts on Load
Frames will be initially empty until their content is hydrated. This can cause unwanted vertical layout shifts as the page loads. To fix this, you can reserve an explicit height on each frame with inline styles:
<local-iframe style="height: 400px;"></local-iframe>You will need to treat local-iframes as block elements for this to work, preferably with inline critical styles in the head of your document:
local-iframe {
display: block;
}You may also set an explicit width, although doing so is optional:
<local-iframe style="width: 800px; height: 400px;"></local-iframe>If you do choose to set an explicit width, make sure you also set a max-width to prevent horizontal overflow on narrower devices:
local-iframe {
display: block;
+ max-width: 100%;
}Fit to Content
To force the outer local-iframe element's height to match the height of the inner iframe content, set the fit-content attribute:
<local-iframe fit-content>
<template>
<p>Really</p>
<p>long</p>
<p>content</p>
<p>that</p>
<p>forces</p>
<p>the inner iframe</p>
<p>to scroll.</p>
<p>But since fit-content is set,</p>
<p>this local-iframe will resize itself</p>
<p>to match the inner iframe's height.</p>
</template>
</local-iframe>[!NOTE] If you also set an inline height on
local-iframelikestyle="height: 200px", thefit-contentattribute will always have the final say. If you later remove thefit-contentattribute, the element will return to the previous height that you set, orautoif one was not previously set.
API
Attributes and Properties
[!TIP] You may either set attributes via HTML or programmatically in JavaScript.
LocalIframeprovides typed getter/setter properties for each attribute listed in the table below. For example:// these are equivalent localIframe.fitContent = true; localIframe.setAttribute("fit-content", ""); // and so are these localIframe.fitContent = false; localIframe.removeAttribute("fit-content");Any attributes of type
booleanare treated asfalseby default, and the presence of the value is all that matters in your HTML.
| Attribute | Type | Description |
| ------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| template | string | The ID of the <template> element to use for the underlying iframe's content. |
| description | string | A title to set on the underlying iframe, for improved accessibility. |
| fit-content | boolean | If this attribute is set, the element will size its height to match the height of the inner iframe document content and watch for any changes. |
Methods
| Method | Type | Description |
| --------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| _render | (html: string) => string | Method on LocalIframe that takes the incoming template HTML and returns an HTML string to set on the underlying iframe.srcdoc attribute. You can override this method in subclasses to define your own HTML. |
Local Development
- Clone this repo.
- Run
pnpm installto install dev dependencies (Vite). - Run
pnpm run devto start the local dev server. - Open http://localhost:5173 in your browser.
FAQs
Sure, nothing is stopping you from doing this:
<iframe srcdoc="<!DOCTYPE html>..."></iframe>But this is inconvenient for several reasons:
- You have to escape special characters like
<,>,", and'yourself. - You lose syntax highlighting and intellisense/autocomplete and it's harder to read.
- You have to repeat the common document structure every time.
local-iframe allows you to write the srcdoc as regular HTML, but inside a <template> so that it never runs in the page context. It's basically a helper/decorator web component.
