眞鍋聡良

https://manabeakira.jp
# プログラム log

Safariでのステータスバーの色調整 〜ダークモード対応〜

2025年2月11日

作ったサイトをiPhoneのSafariで表示してみたとき、サイトに使っている色調と離れた色がステータスバー(時刻や電池残量を表示している画面上部の部分)にしゃしゃり出てきて画面の統一感が崩れることはありませんか?

上に示したのは今回紹介する方法による修正前の私のサイトです。よく見るとステータスバーとサイトの背景色で、同じ白でも色調の違う白が適用されていて、なんか馴染んでいません。

今回はこの問題でお困りのみなさんに向けてのtipsになります。例えば、ステータスバーの色をサイトの背景色と同じ白にしたい場合は、htmlのheadタグ内に

<head>
<meta name="theme-color" content="#F5F5F7">
</head>

を記述すればいいです。safariがtheme-colorに設定された色を読み取って、ステータスバーに反映してくれる仕掛けなんですね。2箇所の色が揃った様子はもう少しスクロールしたところにある動画からどうぞ。(初期で適用されがちな#ffffffが白として一般的なコードですが、少し発光が強くなりすぎるところがあるので、私は#F5F5F7をよく使います。このサイトのライトモード時の背景色も#F5F5F7です〜)

採用する色によっては、ios側で調整がかけられて思い通りに反映されないこともあるとかないとか...なので色々試してみましょう。

ダークモード・ライトモードのテーマ(カラースキーム)を切り替えられるサイトを運営している上級者のみなさんは、もうひと工夫必要です。ダークモード・ライトモードの2通りで、ステータスバーを切り替える必要があるからです。

CSSで @media(prefers-color-scheme: dark) を使ってダークテーマを設定しているみなさんは

<meta name="theme-color" content="#F5F5F7" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#000000FF" media="(prefers-color-scheme: dark)">

などでオッケーです。

ところが、サイト内にJavaScriptを使ったテーマ切り替えボタンも配置しているかたで、html[data-theme="light"] 、html[data-theme="dark"]で切り替えているかた(今の私のサイトです)はこれでは困ったことが起こります。

サイト内の切り替えボタンで手動でテーマ変更した際、デバイスがライトモードでサイトがダークモードだとステータスバーは白色に、逆にデバイスがダークモードでサイトがライトモードだとステータスバーが黒色になってチグハグになるわけです。

これを解決するためには、<meta name="theme-color"> を JavaScript の html[data-theme="light"] 、html[data-theme="dark"] に対応させて、以下のように document.querySelector('meta[name="theme-color"]') を動的に更新するコードを追加すればOKです。

document.addEventListener("DOMContentLoaded", () => {
        const themeMetaTag = document.querySelector('meta[name="theme-color"]');

        const updateThemeColor = (theme) => {
            if (themeMetaTag) {
            themeMetaTag.setAttribute("content", theme === "dark" ? "#000000FF" : "#F5F5F7");
            }
        };

        // テーマ適用時に `theme-color` を更新
        const applyTheme = (theme, save = true) => {
            document.documentElement.setAttribute("data-theme", theme);
            if (save) localStorage.setItem("theme", theme);
            updateThemeColor(theme);
        };

        // 初回読み込み時の適用
        const savedTheme = localStorage.getItem("theme");
        if (savedTheme) {
            applyTheme(savedTheme, false);
        } else {
            const systemPrefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
            applyTheme(systemPrefersDark ? "dark" : "light", false);
        }

        // 切り替えボタン
        const themeToggleButton = document.getElementById("theme-toggle");
        if (themeToggleButton) {
            themeToggleButton.addEventListener("click", () => {
            const currentTheme = document.documentElement.getAttribute("data-theme");
            applyTheme(currentTheme === "dark" ? "light" : "dark");
            });
        }

        // OSのテーマ変更を感知して反映
        const systemTheme = window.matchMedia("(prefers-color-scheme: dark)");
        systemTheme.addEventListener("change", (event) => {
            if (!localStorage.getItem("theme")) {
            applyTheme(event.matches ? "dark" : "light", false);
            }
        });
        });
<head>
    <meta name="theme-color" content="#F5F5F7">
</head>

htmlのhead内には初期値を一つだけ設定して、あとはJavaScriptで変更するようにします。

① themeMetaTag.setAttribute("content", 色) で meta の content を変更

② ページ読み込み時に localStorage or OSの設定 に基づいて theme-color を適用

③ テーマ切り替え時に theme-color も即座に更新

④ OSのテーマ変更 (prefers-color-scheme) を検知して theme-color を自動更新

これでステータスバーもサイトの背景色に調和して切り替わるようになりました。試行錯誤の末、きっちり動くようになってくれると気持ちいいですね!

私のサイトで使用しているLazyLoad(画像遅延読み込み)とダークモード(localStorageに保存&prefers-color-schemeに対応)、テーマ切り替えボタン(太陽と月)に、今回のステータスバー用のtheme-colorの設定を統合した際のHTML、CSS、JavaScriptのコードは以下のようになりました。

JavaScript(headタグ内)
  
(function() {
      const savedTheme = localStorage.getItem('theme');
      if (savedTheme) {
        document.documentElement.setAttribute('data-theme', savedTheme);
      } else {
        const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
        document.documentElement.setAttribute('data-theme', systemPrefersDark ? 'dark' : 'light');
      }
    })();
JavaScript
  
document.addEventListener("DOMContentLoaded", () => {
  // ダーク&ライトテーマの適用
  const themeMetaTag = document.querySelector('meta[name="theme-color"]');
  const themeToggleButton = document.getElementById("theme-toggle");
  const sunIcon = document.getElementById("icon-sun");
  const moonIcon = document.getElementById("icon-moon");

  const updateThemeColor = (theme) => {
    if (themeMetaTag) {
      themeMetaTag.setAttribute("content", theme === "dark" ? "#000000FF" : "#F5F5F7");
    }
  };

  const applyTheme = (theme, save = true) => {
    document.documentElement.setAttribute("data-theme", theme);
    if (save) localStorage.setItem("theme", theme);
    updateThemeColor(theme);

  // アイコンの切り替え
    sunIcon.style.display = theme === "dark" ? "block" : "none";
    moonIcon.style.display = theme === "light" ? "block" : "none";
  };

  const savedTheme = localStorage.getItem("theme");
  if (savedTheme) {
    applyTheme(savedTheme, false);
  } else {
    const systemPrefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
    applyTheme(systemPrefersDark ? "dark" : "light", false);
  }

  if (themeToggleButton) {
    themeToggleButton.addEventListener("click", () => {
      const currentTheme = document.documentElement.getAttribute("data-theme");
      applyTheme(currentTheme === "dark" ? "light" : "dark");
    });
  }

  const systemTheme = window.matchMedia("(prefers-color-scheme: dark)");
  systemTheme.addEventListener("change", (event) => {
    if (!localStorage.getItem("theme")) {
      applyTheme(event.matches ? "dark" : "light", false);
    }
  });

  // LazyLoad+フェードインアニメーション
  const lazyImages = document.querySelectorAll(".lazy");

  const observer = new IntersectionObserver((entries, observer) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        const img = entry.target;

        const onImageLoad = (image) => {
          requestAnimationFrame(() => {
            image.classList.add("fade-in");
            image.style.visibility = "visible";
            image.style.opacity = "1";
          });
        };

        img.onload = () => onImageLoad(img);
        img.src = img.dataset.src;
        img.removeAttribute("data-src");

        if (img.complete) {
          onImageLoad(img);
        }

        observer.unobserve(img);
      }
    });
  });

  lazyImages.forEach((img) => observer.observe(img));
});
HTML

<head>
  <meta name="theme-color" content="#F5F5F7">
</head>

<body>

<button id="theme-toggle" class="toggle-button">
    <svg id="icon-sun" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" class="css-4ym8mv">
      <circle cx="12" cy="12" r="5"></circle>
      <line x1="12" y1="1" x2="12" y2="3"></line>
      <line x1="12" y1="21" x2="12" y2="23"></line>
      <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
      <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
      <line x1="1" y1="12" x2="3" y2="12"></line>
      <line x1="21" y1="12" x2="23" y2="12"></line>
      <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
      <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
    </svg>

    <svg id="icon-moon" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" class="css-4ym8mv">
    <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</button>

  <img class="lazy" data-src="image.jpg" alt="Lazy Image">
</body>
CSS

// ダークモード対応
html[data-theme="dark"] {
  background-color: #000000FF;
  color: #FFFFFFCC;
}

html[data-theme="light"] {
  background-color: #F5F5F7;
  color: #000000FF;
}

// LazyLoad フェードインアニメーション
.lazy {
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.5s ease-in-out;
}

.lazy.fade-in {
  visibility: visible;
  opacity: 1;
}

このサイトの仕様は日々アップデートされているため、上記のコードも記事の作成後に変更されていると想定されます。最新のコードなどは直近の プログラム log で、この記事の作成時の状態での挙動を確認したいかたは https://lab.manabeakira.jp/ui/status-bar-color からお願いします。ソースはパソコンのブラウザで左クリックするなどで見れますよぅ。

コード生成&アイデアまとめ協力:GPT-4-turboさん


公開:2025年2月11日
最終更新:2025年2月11日