---
slug: "wkwebview-opacity-transition-not-working"
title: "WKWebView で CSS の opacity transition が動かないとき疑うこと"
description: "WKWebView 内で CSS `opacity` の transition が動かない (急に消える) ときに疑うこと。`will-change` や `transform: translateZ(0)` でレイヤー化を強制する対処方法。"
url: "https://www.ytyng.com/blog/wkwebview-opacity-transition-not-working"
publish_date: "2026-05-06T11:28:21.114Z"
created: "2026-05-06T11:28:21.115Z"
updated: "2026-05-11T13:10:24.517Z"
categories: []
keywords: ""
featured_image_url: "https://media.ytyng.com/resize/20260506/fe5cdad1e3ca4b1f9a2aeba932f2a568.png.webp?width=768"
has_video: true
has_music: true
video_urls: ["https://media.ytyng.net/ytyng-blog/347/featured-video-1.mp4", "https://media.ytyng.net/ytyng-blog/347/featured-video-2.mp4", "https://media.ytyng.net/ytyng-blog/347/featured-video-3.mp4"]
music_urls: ["https://media.ytyng.net/ytyng-blog/347/featured-music-347-1.mp3", "https://media.ytyng.net/ytyng-blog/347/featured-music-347-2.mp3"]
lang: "ja"
---

# WKWebView で CSS の opacity transition が動かないとき疑うこと

## 症状

WKWebView (macOS / iOS) や同系統のエンジン (Electron, Capacitor, Tauri) で、

- HTML / CSS のロードは成功している
- 画像 URL も取れて Network には 200 OK
- `<div class="layer active">` の `active` クラスもちゃんと付与されている
- なのに **画面に何も表示されない**

CSS は典型的なフェードイン:

```css
.layer { opacity: 0; transition: opacity 1s ease-in-out; }
.layer.active { opacity: 1; }
```

Web Inspector で `getComputedStyle(el).opacity` を見ると `"0"` のまま固まっている。

## 原因

WebKit は **`document.visibilityState === 'hidden'` のとき CSS transition / animation を停止する** 最適化を持っている。具体的には:

- `requestAnimationFrame` を停止
- CSS transition の進行も止める
- `setTimeout` / `setInterval` は throttled

普通にブラウザでタブを開いていれば visible なので問題ないが、WKWebView が **OS から見て表示されてない** 状態だと hidden 扱いになる。

`opacity: 0 → 1` の transition は開始時刻 t=0 のフレームから始まって 1 秒かけて t=1 になるはずが、t=0 で停止してしまう。class は付いているし、CSS の最終値も解釈されているのに、画面上は opacity 0 のまま。

## どこで踏むか

- macOS のスクリーンセーバ (`legacyScreenSaver` プロセス内の WKWebView)
- iOS / iPadOS のバックグラウンドプリロード
- Electron で `BrowserWindow({ show: false })` のまま操作
- Mac Catalyst の背面ウィンドウ
- Capacitor / Tauri の初期表示前のフレーム
- Headless Chrome で screenshot を急いで撮らせている時

## 切り分け

Web Inspector の Console で 4 つチェック:

```js
document.visibilityState                           // → "hidden" ならビンゴ
el.classList.contains('active')                    // → true (= class は付いている)
getComputedStyle(el).opacity                       // → "0" のまま固まっている
el.style.transition = 'none'; el.style.opacity = 1 // → 即座に表示される
```

これが揃ったら確定。

## 解決策 A: WKUserScript で visibility を偽装する

WKWebView 側 (Swift) で `WKUserScript` を仕掛ける。

```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)
)
```

ポイント:

- `document` に直接 `Object.defineProperty` するのではなく **`Document.prototype` 経由** で再定義する。`document` 自体はインスタンスプロパティが既に確定済みのことがある
- `configurable: true` を必ず付ける。再定義時の例外を防ぐため
- `injectionTime: .atDocumentStart` で webapp の JS より早く走らせる

## 解決策 B: CSS で transition を殺す

visibility 偽装が効かない場合や効果が遅れる場合、CSS で直接 transition を切る:

```swift
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)
)
```

A と B を両方仕掛けても害はない。

## 解決策 C: webapp 側に flag を渡す

URL に `?embed=1` のような印を付けて、webapp 側で transition を無効化、初期から `opacity: 1` で render する。長期的にはこれが一番きれい。

```html
<style>
  .layer { opacity: 0; transition: opacity 1s; }
  .layer.active { opacity: 1; }
  body.embed .layer { transition: none; opacity: 1; }
</style>
```

```js
if (new URLSearchParams(location.search).get('embed') === '1') {
  document.body.classList.add('embed');
}
```

埋め込み側 (WKWebView) が webapp の CSS 詳細を知らずに済むので、知識の局在化という意味でも筋が良い。

## まとめ

「class は付いているのに opacity だけ 0」のとき `document.visibilityState` を疑う。WKWebView では `Object.defineProperty(Document.prototype, 'visibilityState', ...)` で偽装可能だが、可能なら webapp 側に flag を渡して対応するのがきれい。`transition: none !important` の CSS 注入を併用すると確実。
