When CSS opacity transition silently fails inside WKWebView
Symptom
In WKWebView (macOS / iOS) or engines of the same family (Electron, Capacitor, Tauri):
- The HTML / CSS loads successfully
- The image URL is fetched too, and Network shows 200 OK
- The
activeclass is properly added to<div class="layer active"> - And yet nothing is shown on screen
The CSS is a typical fade-in.
.layer { opacity: 0; transition: opacity 1s ease-in-out; }
.layer.active { opacity: 1; }
When you look at getComputedStyle(el).opacity in Web Inspector, it is stuck at "0".
Cause
WebKit has an optimization that stops CSS transitions / animations when document.visibilityState === 'hidden'. Specifically:
requestAnimationFrameis stopped- CSS transitions also stop progressing
setTimeout/setIntervalare throttled
If you just have a tab open in a browser, it is visible, so there is no problem. But when the WKWebView is not displayed from the OS's point of view, it is treated as hidden.
The opacity: 0 → 1 transition should start from the frame at start time t=0 and take 1 second to reach t=1, but it stops at t=0. The class is applied and the final CSS value is parsed, but on screen the opacity stays at 0.
Where you hit this
- macOS screensavers (a WKWebView inside the
legacyScreenSaverprocess) - Background preloading on iOS / iPadOS
- Operating Electron with
BrowserWindow({ show: false })still in place - Background windows in Mac Catalyst
- Frames before the first display in Capacitor / Tauri
- When you make Headless Chrome take a screenshot in a hurry
How to confirm
Check four things in the Web Inspector Console.
document.visibilityState // → "hidden" → bingo
el.classList.contains('active') // → true
getComputedStyle(el).opacity // → "0" stuck
el.style.transition = 'none'; el.style.opacity = 1 // → instantly visible
If all of these match, it is confirmed.
Fix A: spoof visibility from the native side
Set up a WKUserScript on the WKWebView side (Swift).
let visibilityOverride = """
try {
Object.defineProperty(Document.prototype, 'hidden', {
configurable: true, get: function() { return false; }
});
Object.defineProperty(Document.prototype, 'visibilityState', {
configurable: true, get: function() { return 'visible'; }
});
} catch (e) {}
"""
config.userContentController.addUserScript(
WKUserScript(source: visibilityOverride,
injectionTime: .atDocumentStart,
forMainFrameOnly: false)
)
The points are as follows.
- Do not call
Object.definePropertydirectly ondocument. Redefine it throughDocument.prototype. Ondocumentitself, the instance properties are sometimes already fixed - Always add
configurable: true. This prevents an exception on redefinition - Use
injectionTime: .atDocumentStartto run it earlier than the webapp's JS
Fix B: kill the transition with injected CSS
If spoofing visibility does not work, or its effect comes late, turn off the transition directly with CSS.
let cssOverride = """
(function(){
var css = '.layer { transition: none !important; }' +
'.layer.active { opacity: 1 !important; }';
var apply = function(){
if (!document.head) return false;
var s = document.createElement('style');
s.textContent = css;
document.head.appendChild(s);
return true;
};
if (!apply()) document.addEventListener('DOMContentLoaded', apply);
})();
"""
config.userContentController.addUserScript(
WKUserScript(source: cssOverride,
injectionTime: .atDocumentEnd,
forMainFrameOnly: true)
)
There is no harm in setting up both A and B.
Fix C: hand the webapp a flag
Add a marker like ?embed=1 to the URL, and on the webapp side disable the transition and render with opacity: 1 from the start. In the long run this is the cleanest.
<style>
.layer { opacity: 0; transition: opacity 1s; }
.layer.active { opacity: 1; }
body.embed .layer { transition: none; opacity: 1; }
</style>
if (new URLSearchParams(location.search).get('embed') === '1') {
document.body.classList.add('embed');
}
The embedding side (WKWebView) does not need to know the details of the webapp's CSS, so it is also a sound approach in the sense of keeping knowledge local.
Related posts
Building a WebView screensaver on macOS 26 without Xcode
Struggling to Understand the Relationship Between Frame Size and Viewport Size in UIWebView
Tips for Making UIWebView Look More Native
Get the Effective Height and Width of the Browser When the Software Keyboard Appears in iOS Safari and Adjust the Layout
We look forward to discussing your development needs.