Today we’re talking about how transparent iframes, dark mode, unreadable text and varying levels of browser support are doing my head in. But don’t worry, there’s light mode light at the end of the tunnel.

And just so you know what you’re missing, I workshoped other titles like “Transparent iframes considered darkful” (always wanted to do one of those), “A plague of dark mode iframes” (reference 1) and “Darkmode on the frame of town” (reference 2).

Iframes, transparent by default

I work at StackBlitz, an online IDE for Web stuff, and like many online code editors1 we typically render two HTML documents:

  1. A top-level document on the stackblitz.com origin, showing a code editor and other tools.
  2. A “preview” document in an iframe, hosted on a different origin (for your own safety), that can be virtually anything that authors of StackBlitz projects generate2: arbitrary HTML and CSS, text/plain HTTP responses, SVG documents, etc.

This is where stuff gets tricky.

By default, iframes are transparent. If you don’t specify a background-color in the embedded document (say, on the html element or on body), the background color from the parent page shows through.

So if the background in the parent document is dark, and the document in the iframe uses browser defaults like black text and a transparent background, you get black text on a dark background and can’t read anything. Whoops.

Because our users were used to web pages with default black text and white backgrounds, the fix was simple: we styled our preview iframes to have a white background.

Most quick demo pages would have black or dark text, resulting in black text on a white background. And more stylish demos would set their own text and background colors, managing color contrast and readability themselves. Problem fixed.

Introducing: color-scheme

A year ago, I tweaked the HTML for all stackblitz.com pages to include a couple meta tags:

<meta name="color-scheme" content="dark">
<meta name="theme-color" content="#2e3138">

These values match the default dark theme (nicknamed “DarkBlitz”) that we use for our editor. When users select the light theme instead in our UI, we then update those meta tags at runtime:

const theme = getTheme(localStorage.currentTheme);
setMetaTag('theme-color', theme.themeColor);
setMetaTag('color-scheme', theme.colorScheme);

(Though it looks like setting the color-scheme property in CSS might be compatible with more browsers, because not all browsers seem to pick up on the meta tag change.)

Why tell browsers about the color scheme used by the page, you ask? Well, quoting MDN:

The color-scheme CSS property allows an element to indicate which color schemes it can comfortably be rendered in. (…) When a user selects one of these color schemes, the operating system makes adjustments to the user interface. This includes form controls, scrollbars, and the used values of CSS system colors.

Our rough goal was to hint to browsers that if they render a scrollbar or a checkbox or a <select> dropdown, we’d appreciate 😘 if they can do that using a theme that is either light or dark (as declared).

How does that relate to iframes? Coming up right now.

Attack of the dark mode iframes

Now, authors using our IDE could also add <meta name="color-scheme" content="dark"> or the equivalent CSS to their pages. Browsers would react by making the page’s text white, and its background dark.

Except in iframes, where the text would be white, but the background would remain transparent. Resulting in, you guessed it, white text on our white background.

That issue was raised with the CSS Working Group, and the resolution was:

If the color scheme of an iframe differs from embedding document, iframe gets an opaque canvas background appropriate to its color scheme.

So the dark mode embedded document, with its <meta name="color-scheme" content="dark"> meta tag, would get a browser style similar to :root { background-color: canvas }, where canvas would be a dark color. White text on dark background, problem solved!

Alas!

Remember that we also have <meta name="color-scheme" content="dark"> in the editor page? So we now have a dark color-scheme in both the embedding document and the embedded document, so the iframe stays transparent. White text on white background again.

Okay, so maybe it was silly to use a white background all along? But remember that all this color-scheme stuff was specified in recent years and implemented in browsers as lately as 2021. For most users and most browsers, we needed that white background, as did CodePen (yup, also setting a white background on their preview iframes) and probably others.

So, what’s the way forward? Do we need to predict what the text color will be and if the iframe will be transparent or not? In our case, we can’t know about what gets rendered in the iframe, so tough luck.

But maybe we don’t need to control or predict that content. We can just say “hey, here’s what we support”, and let the browser handle it? Again, quoting that resolution:

If the color scheme of an iframe differs from embedding document, iframe gets an opaque canvas background appropriate to its color scheme.

So we tell the browser that the embedding document only supports a dark color scheme, and we use a dark background behind the iframe. It could be as simple as:

:root.theme-dark { color-scheme: dark }
:root.theme-light { color-scheme: light }

.preview-iframe {
  /* canvas color depends on the inherited color-scheme */
  background-color: canvas;
}

But if we want to keep a white background for browsers that don’t support color-scheme, it might be safer to do something like:

/* Keep the default white background for backwards compat */
.preview-iframe {
  background-color: white;
}

@supports (color-scheme: dark) {
  :root.theme-dark {
    color-scheme: dark;
  }
  :root.theme-dark .preview-iframe {
    background-color: black;
  }
}

@supports (color-scheme: light) {
  :root.theme-light {
    color-scheme: light;
  }
  :root.theme-light .preview-iframe {
    background-color: white;
  }
}

Would that approach work? Theoretically, yes.

In quick tests in Chrome, it seems to be working well. We might even try it on prod (and roll back if it creates more issues than it solves).

But currently, there are a few browser bugs that might make this solution unreliable:

  • Firefox doesn’t seem to support adding an opaque background to iframes with mismatched color-scheme values.
  • There might be some smaller issues in Chrome as well, according to the description in this test case (but I couldn’t reproduce on macOS with dark and light OS modes).

That Firefox issue means that iframes would always be transparent in Firefox. So using a dark background behind the iframe would break any embedded page that specifies neither a background-color nor a color-scheme in Firefox, and that’s most pages. No good.

We’d have to apply that solution to browser that are known to implement that opaque background logic, and since @supports (color-scheme: light) doesn’t test for that, that means doing some UA sniffing. And I really don’t want to do that.

Quick fix: the least bad solution?

This week, we announced a partnership with Cloudflare, who are using StackBlitz to power demos and tutorials of their Wrangler tool for Cloudflare Workers. Many of the demos were returning text/plain responses, which were shown in our preview iframe.

What’s the color-scheme for a document generated for a text/plain response? Well, that’s entirely up to the browser. Chrome is apparently creating a HTML document like this:

<html>
  <head>
    <meta name="color-scheme" content="light dark">
  </head>
  <body>
    <pre style="word-wrap: break-word; white-space: pre-wrap;">Hello World</pre>
  </body>
</html>

It declares that this document supports both light and dark schemes, and the browser resolves that to either light or dark depending on the OS color scheme, not the parent page’s color-scheme (at least in my tests with Chrome 101 on macOS).

So if:

  • you use a dark mode OS (or browser theme maybe?),
  • and the default dark theme for the StackBlitz editor,
  • and the parent page has <meta name="color-scheme" content="dark">,

… then you get a transparent iframe in Chrome, with white text, on our white background. The white background that we can’t change to a dark one, because that would break a bunch of content in Firefox.

So we went for a quick and dirty fix: we removed the color-scheme meta tag on the parent page.

Now Chrome resolves the color schemes to color-scheme: normal for the parent page and color-scheme: dark for the iframe. And since those don’t match, Chrome makes the iframe opaque (white text, dark canvas background).

Meanwhile, Firefox generates a different kind of document for text/plain responses, and that uses a dark theme (when the OS theme is dark) when opened as the top-level document, but not when rendered as a child document in an iframe. So the text is black, the iframe is transparent, and our background is white, and that works, kinda.

I think that the last remaining bug we have is: if the iframed document sets color-scheme: dark and no background-color, we’ll have white text on a white background in Firefox. Not great, but here’s hoping that Firefox implements the opaque iframe logic soon.

Then maybe we can add color-scheme back to our top-level document, and get its purported benefits.

Better fix: white background, local color-scheme!

Alternatively, we might be able to restore our usage of color-scheme for our pages if we declare that the iframe element uses a light scheme.

:root.theme-dark {
  color-scheme: dark;
}

.preview-iframe {
  /* tell browsers to make the iframe opaque
     for documents with color-scheme:dark */
  color-scheme: light;
  background-color: white;
}

That works in Chrome, i.e. when the embedded document has a color-scheme other than dark, then the iframe is opaque. It also seems to match the text of the spec:

In order to preserve expected color contrasts, in the case of embedded documents typically rendered over a transparent canvas (such as provided via an HTML <iframe> element), if the used color scheme of the element and the used color scheme of the embedded document’s root element do not match, then the UA must use an opaque canvas of the Canvas color appropriate to the embedded document’s used color scheme instead of a transparent canvas.

(Emphasis mine.) If “the element” in that paragraph means the element used to embed a document, i.e. our preview <iframe> element here, it looks like we’re following the spec correctly by setting background-color: white; color-scheme: light; on that <iframe> element.

Should it be simpler?

So here we are. I think I mostly figured it out, in big part thanks to Amelia Bellamy-Royds’s generous help. And there’s hope that things will improve in browsers and we’ll be able to use color-scheme again.

But I’m wondering, should we be jumping through those hoops? Can’t we have a way to ask browsers “Hey, could you handle this iframe’s background for me, since you know what’s going on there and I don’t? Thanks, you’re a peach.” Or, conversely, “Look, I want this iframe to be transparent no matter what, and I’m taking on the responsibility here”.

Right now, the opaque iframe background mechanism is something that:

  1. requires expert knowledge to opt in (it’s far from obvious that the answer to “how do I get the browser to set its default background-color for this page in an iframe, like it does when opening the same page in a new tab?” is “use a mismatched color-scheme value on purpose”);
  2. doesn’t have a way to opt out.

Is it good enough as-is, or could that improve? And if we want to improve at least some of these problems, how could we do it?

We should probably document things more, let’s say by updating MDN to add a new section on the iframe page and another one on the color-scheme page?

On top of that, should there be an explicit way to manage iframe transparency, let’s say with a HTML attribute for the <iframe> element or with a CSS property (iframe-transparency: auto | transparent | opaque or something)?


  1. I also like CodePen for quick CSS tests, and I hear that CodeSandbox and GitPod are pretty good too! 

  2. Especially since we launched WebContainers, which let you run Node.js code in the browser — so you could have an express server returning any type of HTTP response for that preview iframe.