{"slug": "my-zero-js-page-transitions-flashed-white-two-bugs-two-causes-one-blend-mode-i-d", "title": "My zero-JS page transitions flashed white. Two bugs, two causes, one blend mode I'd never heard of.", "summary": "A developer building the accessible multi-page site accessible-by-default.dev discovered that custom CSS view transitions caused white flashes and brightness pulses due to the default mix-blend-mode: plus-lighter. The blend mode, designed for the default cross-fade where opacities sum to one, added colors when the custom animation kept the new page at full opacity, causing clipping to white. The fix involved setting mix-blend-mode to normal on the view transition layers.", "body_md": "**The short version, if you arrived here from a search:**\n\n`mix-blend-mode: plus-lighter`\n\n, which was chosen for the animation you just deleted. That is your brightness pulse.The long version follows, because the reasoning is more useful than the fixes.\n\nMy site [accessible-by-default.dev](https://accessible-by-default.dev/) is a multi-page app on purpose. Real page loads mean screen readers announce the new page, focus resets, the URL bar tells the truth, and there is no routing JavaScript to break. It is an overview hub plus four chapter pages, and I wanted moving between them to feel calm: the frame stays put, the content quietly fades.\n\nThe platform hands you exactly this. One [at-rule](https://developer.mozilla.org/en-US/docs/Web/CSS/@view-transition) in both documents:\n\n```\n@view-transition {\n  navigation: auto;\n}\n```\n\nThe browser snapshots the old page, loads the new one, and animates between them, across a real navigation, with no JavaScript.\n\nThen I customised the animation and got flashes. Bright pulses mid-fade in light mode, odd glare in dark mode, and separately a hard white blink on some navigations. I spent a while chasing them as one bug. They were not one bug.\n\nI could not reproduce any of it in my e2e setup, for an unhelpful reason: the Chromium build my test runner drives does not execute cross-document view transitions at all. Pages swapped instantly. No snapshots, nothing to observe, nothing to debug.\n\nI ended up driving my real installed Chrome over the DevTools protocol to see what users see.\n\nSo: if your transition works locally but not in tests, or passes tests but misbehaves locally, verify that your automation browser actually performs cross-document view transitions before you form a single hypothesis about your CSS. Mine did not, and every conclusion I drew from those runs was noise.\n\nMy custom animation is deliberately simple. The new page renders immediately underneath, and the old page's snapshot fades out on top of it:\n\n```\n::view-transition-new(root) {\n  animation: none;\n}\n\n::view-transition-old(root) {\n  z-index: 1;\n  animation: 0.28s ease both vt-content-fade;\n}\n\n@keyframes vt-content-fade {\n  to { opacity: 0; }\n}\n```\n\nFade one opaque layer over another opaque layer. There is no frame in that description which should produce something brighter than either page. And yet, a visible brightness pulse, on every navigation.\n\nThe answer is in [the browser's own stylesheet](https://drafts.csswg.org/css-view-transitions-1/#ua-styles). The spec places the old and new snapshots inside an isolated group and blends them with `mix-blend-mode: plus-lighter`\n\n, which *adds* the colours of the layers together, weighted by alpha.\n\nThat default is a good decision. In the browser's default cross-fade, the old snapshot's opacity runs 1 to 0 while the new one runs 0 to 1, so the weights always sum to one, and plus-lighter makes the crossfade mathematically seamless: two half-transparent copies of similar content add back up to full brightness instead of dipping grey in the middle.\n\nBut I had replaced the default animation. My new snapshot sits at full opacity for the entire transition, and early in the fade the old snapshot is still nearly opaque on top of it. Plus-lighter adds them. Almost double the light, which on a light theme clips straight to white.\n\nSo the pulse was a blend mode doing precisely what it was told, under an assumption my keyframes had quietly invalidated.\n\nThe fix is to restore normal painting:\n\n```\n::view-transition-image-pair(root) {\n  isolation: auto;\n}\n\n::view-transition-old(root),\n::view-transition-new(root) {\n  mix-blend-mode: normal;\n}\n```\n\nThe generalisable version: if you replace the default view-transition animation with anything whose opacities do not sum to one, you have also inherited a blend mode designed for the animation you just deleted.\n\nAfter the blend fix, something was still there. A pure white flash on some navigations, and this one was fast enough that I could not tell where in the sequence it sat.\n\nStretching the transition to 1.5 seconds settled it: the flash happened first, then the fade played cleanly. It was not part of the transition at all.\n\nIt was the incoming document. During the handoff there can be a painted frame of the new page before its CSS has been applied, and an unstyled document paints the user-agent default, which is white. In dev this was worst, because Vite injects CSS through JavaScript, but a cold navigation in production can catch it too.\n\nThe fix is unglamorous and reliable. Every entry HTML inlines one declaration, so that even a completely unstyled frame paints in the page's background colour rather than white:\n\n```\n<style>\n  @layer base {\n    :root { --boot-ground: light-dark(#f5f5f5, #1a1a1a); }\n    html { background-color: var(--bp-ground, var(--boot-ground)); }\n  }\n</style>\n```\n\nThose two hex values are the light and dark background colours of the blueprint theme — the \"ground\" the rest of the page sits on. The declaration lives in the lowest cascade layer, so the moment the real stylesheet arrives, the theme's own ground token wins through the `var()`\n\nfallback chain. Before that moment, the raw frame is already roughly the right colour.\n\nTwo related details if you build this. Give the entry script `blocking=\"render\"`\n\n, so the first rendered frame — and therefore the snapshot the transition captures — is the full page rather than an empty mount point. And check that your bundler preserves the attribute: Vite dropped it from the built HTML, which took a four-line plugin to put back.\n\nSudden full-screen luminance changes are not a cosmetic issue. For photosensitive users they are the exact thing [WCAG's flash criteria](https://www.w3.org/WAI/WCAG22/Understanding/three-flashes-or-below-threshold.html) exist to prevent, and a page that blinks bright white between navigations is a worse experience than a page with no transition at all.\n\nWhich is why the whole feature is an enhancement rather than a baseline:\n\n```\n@media (prefers-reduced-motion: no-preference) {\n  /* the entire transition setup lives in here */\n}\n```\n\nReduced-motion users get instant, ordinary navigations. Everyone else gets a 0.28 second content fade on top of real multi-page semantics: proper page announcements, focus reset, a working back button, no router.\n\nTotal change for two multi-day bugs: six declarations and one inline style block.\n\nYou can feel it on the [live site](https://accessible-by-default.dev/), and the source is here:\n\nHow much of accessibility does the modern web platform handle **natively** —\nwith little to no JavaScript? This site is that question, answered as one\nargument in four parts: what **the standard** (WCAG) asks for, **the craft**\nof meeting it with modern CSS and HTML, what cutting-edge **CSS** makes\npossible next, and **the proof** that it holds up.\n\nUnderneath it is an accessibility-first styling foundation — SCSS mixins\ndesign tokens, and a cascade-layer architecture — where components adapt to\n**user preferences** (reduced motion, high contrast, forced colors, dark mode\nreduced transparency) and **input capabilities** (hover, touch) by default,\nwith the cascade doing the work instead of `!important`\n\n.\n\nNote\n\n**The design vote is settled.** The blueprint restructure — an overview hub\nplus four chapter pages, wearing a technical-drawing look — won the review\nround and is now the live design. The previous single-page design is kept\nunder…\n\nIf you have hit a third cause of flashing in a cross-document transition, I would like to hear about it. Two took me long enough that a catalogue seems worth starting.", "url": "https://wpnews.pro/news/my-zero-js-page-transitions-flashed-white-two-bugs-two-causes-one-blend-mode-i-d", "canonical_source": "https://dev.to/thomas_sweet/my-zero-js-page-transitions-flashed-white-two-bugs-two-causes-one-blend-mode-id-never-heard-of-5g77", "published_at": "2026-07-31 19:48:46+00:00", "updated_at": "2026-07-31 20:11:28.508924+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["accessible-by-default.dev", "Chromium", "Chrome", "DevTools protocol", "CSS", "MDN", "W3C"], "alternates": {"html": "https://wpnews.pro/news/my-zero-js-page-transitions-flashed-white-two-bugs-two-causes-one-blend-mode-i-d", "markdown": "https://wpnews.pro/news/my-zero-js-page-transitions-flashed-white-two-bugs-two-causes-one-blend-mode-i-d.md", "text": "https://wpnews.pro/news/my-zero-js-page-transitions-flashed-white-two-bugs-two-causes-one-blend-mode-i-d.txt", "jsonld": "https://wpnews.pro/news/my-zero-js-page-transitions-flashed-white-two-bugs-two-causes-one-blend-mode-i-d.jsonld"}}