如何覆盖 css 首选配色方案设置

How to override css prefers-color-scheme setting

我正在实现深色模式,因为 macOS、Windows 和 iOS 都引入了深色模式。

Safari、Chrome 和 Firefox 有一个本机选项,使用以下 CSS 媒体规则:

@media (prefers-color-scheme: dark) {
body {
    color:#fff;
    background:#333333
}

这将自动识别设置为黑暗模式的系统,并应用随附的 CSS 规则。

但是;即使用户可能将他们的系统设置为深色模式,他们也可能更喜欢特定网站的浅色或默认主题。还有 Microsoft Edge 用户不(还)支持 @media (prefers-color-scheme 的情况。为了获得最佳用户体验,我想确保这些用户可以在这些情况下在暗模式和默认模式之间切换。

是否有可以执行此操作的方法,可能使用 HTML 5 或 JavaScript?我会包含我尝试过的代码,但我一直无法找到任何关于实现它的信息!

我已经确定了一个合适的解决方案,如下:

CSS 将使用变量和主题:

// root/default variables
:root {
    --font-color: #000;
    --link-color:#1C75B9;
    --link-white-color:#fff;
    --bg-color: rgb(243,243,243);
}
//dark theme
[data-theme="dark"] {
    --font-color: #c1bfbd;
    --link-color:#0a86da;
    --link-white-color:#c1bfbd;
    --bg-color: #333;
}

然后在必要时调用变量,例如:

//the redundancy is for backwards compatibility with browsers that do not support CSS variables.
body
{
    color:#000;
    color:var(--font-color);
    background:rgb(243,243,243);
    background:var(--bg-color);
}

JavaScript 用于识别用户设置的主题,或者如果他们有 over-ridden 他们的 OS 主题,以及在两者之间切换,这包括在内在header之前输出html<body>...</body>:

//determines if the user has a set theme
function detectColorScheme(){
    var theme="light";    //default to light

    //local storage is used to override OS theme settings
    if(localStorage.getItem("theme")){
        if(localStorage.getItem("theme") == "dark"){
            var theme = "dark";
        }
    } else if(!window.matchMedia) {
        //matchMedia method not supported
        return false;
    } else if(window.matchMedia("(prefers-color-scheme: dark)").matches) {
        //OS theme setting detected as dark
        var theme = "dark";
    }

    //dark theme preferred, set document with a `data-theme` attribute
    if (theme=="dark") {
         document.documentElement.setAttribute("data-theme", "dark");
    }
}
detectColorScheme();

此javascript用于在设置之间切换,它不需要包含在页面的header中,但可以包含在任何地方

//identify the toggle switch HTML element
const toggleSwitch = document.querySelector('#theme-switch input[type="checkbox"]');

//function that changes the theme, and sets a localStorage variable to track the theme between page loads
function switchTheme(e) {
    if (e.target.checked) {
        localStorage.setItem('theme', 'dark');
        document.documentElement.setAttribute('data-theme', 'dark');
        toggleSwitch.checked = true;
    } else {
        localStorage.setItem('theme', 'light');
        document.documentElement.setAttribute('data-theme', 'light');
        toggleSwitch.checked = false;
    }    
}

//listener for changing themes
toggleSwitch.addEventListener('change', switchTheme, false);

//pre-check the dark-theme checkbox if dark-theme is set
if (document.documentElement.getAttribute("data-theme") == "dark"){
    toggleSwitch.checked = true;
}

最后,用于在主题之间切换的 HTML 复选框:

<label id="theme-switch" class="theme-switch" for="checkbox_theme">
    <input type="checkbox" id="checkbox_theme">
</label>

通过使用CSS变量和JavaScript,我们可以自动确定用户主题,应用它,并允许用户也over-ride它。 [截至撰写本文时(2019/06/10),只有 Firefox 和 Safari 支持自动主题检测]

采用@JimmyBanks 提供的解决方案,1) 将复选框变成切换文本按钮,2) 在 OS 主题更改时添加自动主题切换。

CSS 不变,浅色主题存储在 :root 中,深色主题存储在 [data-theme="dark"]:

:root {
  --color_01: #000;
  --color_02: #fff;
  --color_03: #888;
}

[data-theme="dark"] {
  --color_01: #fff;
  --color_02: #000;
  --color_03: #777;
}

<head> JS 有一些编辑,包括一些遗漏和将 data-theme 语句移动到后续 JS 块:

var theme = 'light';
if (localStorage.getItem('theme')) {
  if (localStorage.getItem('theme') === 'dark') {
    theme = 'dark';
  }
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
  theme = 'dark';
}

这里是对 JS 的第二个块的编辑,加上相关的 HTML。 theme_switch 切换主题,而 theme_OS 会根据 OS 主题的更改自动更新网站的主题。

var theme;
function theme_apply() {
  'use strict';
  if (theme === 'light') {
    document.getElementById('theme_readout').innerHTML = 'Dark';
    document.documentElement.setAttribute('data-theme', 'light');
    localStorage.setItem('theme', 'light');
  } else {
    document.getElementById('theme_readout').innerHTML = 'Light';
    document.documentElement.setAttribute('data-theme', 'dark');
    localStorage.setItem('theme', 'dark');
  }
}
theme_apply();
function theme_switch() {
  'use strict';
  if (theme === 'light') {
    theme = 'dark';
  } else {
    theme = 'light';
  }
  theme_apply();
}
var theme_OS = window.matchMedia('(prefers-color-scheme: light)');
theme_OS.addEventListener('change', function (e) {
  'use strict';
  if (e.matches) {
    theme = 'light';
  } else {
    theme = 'dark';
  }
  theme_apply();
});
<a onclick="theme_switch()">Theme: <span id="theme_readout"></span></a>

如果您有任何改进建议,请告诉我!

您可以使用我的自定义元素<dark-mode-toggle> that initially adheres to the user's prefers-color-scheme setting, but that also allows the user to (permanently or temporarily) override it. The toggle works both with separate CSS files or with classes that are toggled. The README有两种方法的示例。

我的解决方案(无线电输入中的 3 个选项:暗、系统、亮)改编自 JimmyBanks 和 Meanderbilt 解决方案:

我猜它有点冗长,但我费了点劲才想明白它

const themeSwitches = document.querySelectorAll('[data-color-theme-toggle]')

function removeColorThemeLocalStorage() {
  localStorage.removeItem('color-theme')
}

function saveColorTheme(colorTheme) {
  if (colorTheme === 'system') {
    removeColorThemeLocalStorage()
    return
  }
  localStorage.setItem('color-theme', colorTheme)
}

function applyColorTheme() {
  const localStorageColorTheme = localStorage.getItem('color-theme')
  const colorTheme = localStorageColorTheme || null
  if (colorTheme) {
    document.documentElement.setAttribute('data-color-theme', colorTheme)
  }
}

function themeSwitchHandler() {
  themeSwitches.forEach(themeSwitch => {
    const el = themeSwitch
    if (el.value === localStorage.getItem('color-theme')) {
      el.checked = true
    }

    el.addEventListener('change', () => {
      if (el.value !== 'system') {
        saveColorTheme(el.value)
        applyColorTheme(el.value)
      } else {
        removeColorThemeLocalStorage()
        document.documentElement.removeAttribute('data-color-theme')
      }
    })
  })
  applyColorTheme()
}
document.addEventListener('DOMContentLoaded', () => {
  themeSwitchHandler()
  applyColorTheme()
})

html {
  --hue-main: 220;
  --color-text: hsl(var(--hue-main), 10%, 25%);
  --color-text--high-contrast: hsl(var(--hue-main), 10%, 5%);
  --color-link: hsl(var(--hue-main), 40%, 30%);
  --color-background: hsl(var(--hue-main), 51%, 98.5%);
}

@media (prefers-color-scheme: dark) {
  html.no-js {
    --color-text: hsl(var(--hue-main), 5%, 60%);
    --color-text--high-contrast: hsl(var(--hue-main), 10%, 80%);
    --color-link: hsl(var(--hue-main), 60%, 60%);
    --color-background: hsl(var(--hue-main), 10%, 12.5%);
  }
}

[data-color-theme='dark'] {
  --color-text: hsl(var(--hue-main), 5%, 60%);
  --color-text--high-contrast: hsl(var(--hue-main), 10%, 80%);
  --color-link: hsl(var(--hue-main), 60%, 60%);
  --color-background: hsl(var(--hue-main), 10%, 12.5%);
}
    <div class="color-scheme-toggle" role="group" title="select a color scheme">
    <p>saved setting: <span class="theme-readout">...</span></p>
        <input type="radio" name="scheme" id="dark" value="dark" aria-label="dark color scheme"> <label for="dark">dark</label>
        <input type="radio" name="scheme" id="system" value="system" aria-label="system color scheme" checked="system"> <label for="system">system</label>
        <input type="radio" name="scheme" id="light" value="light" aria-label="light color scheme"> <label for="light">light</label>
    </div>

这是一个遵循默认 prefers-color-scheme 的答案,然后才允许您通过 localStorage 进行切换。这节省了通过 JS 找出默认方案所需的时间,而且即使没有 JS,人们也会使用默认方案。

我不喜欢必须声明默认样式(我选择了深色)然后 re-declaring 它作为 class 称为 dark-mode 但它是 unavoidable .

请注意,此论坛似乎已屏蔽 localStorage,因此您必须在其他地方尝试该代码。

var theme, prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)");
if (prefersDarkScheme.matches)
    theme = document.body.classList.contains("light-mode") ? "light" : "dark";
else
    theme = document.body.classList.contains("dark-mode") ? "dark" : "light";
localStorage.setItem("theme", theme);

function toggle() {
    var currentTheme = localStorage.getItem("theme");
    if (currentTheme == "dark")
        document.body.classList.toggle("light-mode");
    else if (currentTheme == "light")
        document.body.classList.toggle("dark-mode");
}
.dark-mode {color: white; background-color: black}
.dark-mode a:link {color: DeepSkyBlue}
.light-mode {color: black; background-color: white}
.light-mode a:link {color: green}


@media (prefers-color-scheme: dark) {
    body {color: white; background-color: black}
    a:link {color: DeepSkyBlue}
}
<button onclick="toggle()">Toggle Light/Dark Mode</button>

None 以上适合我。我决定从不同的角度来解决这个问题。年份是 2021 年。


以下优惠:

  • 尊重系统偏好。
  • 系统偏好设置覆盖。
  • 滚动条配色方案。
  • 通用浏览器支持。(IE 生命周期结束,2021 年 8 月 17 日✌️)

当您查看 MDN Web Docs page for prefers-color-scheme 时,您可以阅读以下内容:

The prefers-color-scheme CSS media feature is used to detect if the user has requested a light or dark color theme. [...]

light Indicates that user has notified that they prefer an interface that has a light theme, or has not expressed an active preference.

因此对于任何浏览器,默认情况下 prefers-color-scheme 设置为 light 或不受支持。

我在接受答案时遇到的一个问题是更改没有影响滚动条颜色。这可以使用耦合到 :root 伪元素的 color-scheme CSS property 来处理。

我遇到的另一个问题是,如果用户将系统设置更改为浅色或深色,网站不会受到影响,并且会在两种样式之间产生不匹配。我们可以通过将 window.matchMedia( '(prefers-color-scheme: light)' ) 耦合到 onchange 事件侦听器来修复该行为。

这是最终脚本。

(() => {
    var e = document.getElementById("tglScheme");
    window.matchMedia("(prefers-color-scheme: dark)").matches
        ? (document.head.insertAdjacentHTML("beforeend", '<style id="scheme">:root{color-scheme:dark}</style>'),
          document.body.classList.add("dark"),
          e && (e.checked = !0),
          window.localStorage.getItem("scheme") &&
              (document.getElementById("scheme").remove(), document.head.insertAdjacentHTML("beforeend", '<style id="scheme">:root{color-scheme:light}</style>'), document.body.classList.remove("dark"), e && (e.checked = !1)),
          e &&
              e.addEventListener("click", () => {
                  e.checked
                      ? (document.getElementById("scheme").remove(),
                        document.head.insertAdjacentHTML("beforeend", '<style id="scheme">:root{color-scheme:dark}</style>'),
                        document.body.classList.add("dark"),
                        localStorage.removeItem("scheme"))
                      : (document.getElementById("scheme").remove(),
                        document.head.insertAdjacentHTML("beforeend", '<style id="scheme">:root{color-scheme:light}</style>'),
                        document.body.classList.remove("dark"),
                        localStorage.setItem("scheme", 1));
              }))
        : (document.head.insertAdjacentHTML("beforeend", '<style id="scheme">:root{color-scheme:light}</style>'),
          e && (e.checked = !1),
          window.localStorage.getItem("scheme") &&
              (document.getElementById("scheme").remove(), document.head.insertAdjacentHTML("beforeend", '<style id="scheme">:root{color-scheme:dark}</style>'), document.body.classList.add("dark"), e && (e.checked = !0)),
          e &&
              e.addEventListener("click", () => {
                  e.checked
                      ? (document.getElementById("scheme").remove(),
                        document.head.insertAdjacentHTML("beforeend", '<style id="scheme">:root{color-scheme:dark}</style>'),
                        document.body.classList.add("dark"),
                        localStorage.setItem("scheme", 1))
                      : (document.getElementById("scheme").remove(),
                        document.head.insertAdjacentHTML("beforeend", '<style id="scheme">:root{color-scheme:light}</style>'),
                        document.body.classList.remove("dark"),
                        localStorage.removeItem("scheme"));
              }));
})(),
window.matchMedia("(prefers-color-scheme: light)").addEventListener("change", () => {
    location.reload(), localStorage.removeItem("scheme");
});

对于CSS这边,我们使用默认的variable custom property values fallback,第一个位置是深色。我们可以通过 :root 元素定义所有必要的深色。

:root body.dark {
  --app-bg-dark: #131313;
  --app-tx-dark: #f8f9fa;
}
body{
  background-color: var( --app-bg-dark, white );
  color: var( --app-tx-dark, black );
}
/* if dark mode isn't set, fall back to light. */

对于 html,一个简单的复选框 <input id="tglScheme" type="checkbox">

终于来了 Codepen https://codepen.io/amarinediary/full/yLgppWW

⚠️️ Codepen 覆盖 location.reload() 所以你将无法测试系统更改的实时更新。不要犹豫,在您的本地主机上尝试一下。

我认为最好的方法是本机遵循系统设置,除非用户另有说明。

在 html 中创建按钮。然后用js给它绑定三位开关。保存到浏览器的 LocalStorage。

最后,风格化你的开关元素。

document.addEventListener("DOMContentLoaded", function(event) {
  switchTheme('.theme-switch');
});

function switchTheme(selector) {
  const switches = document.querySelectorAll(selector);
  // let colorTheme = localStorage.getItem('colorTheme') || 'system'; //commented to avoid security issue
  let colorTheme = 'system';

  function changeState() {
    // localStorage.setItem('colorTheme', colorTheme); //commented to avoid security issue
    document.documentElement.setAttribute('data-theme', colorTheme);
  }
  changeState();

  switches.forEach(el => {
    el.addEventListener('click', () => {
      switch (colorTheme) {
        case 'dark':
          colorTheme = 'light';
          break
        case 'light':
          colorTheme = 'system';
          break
        default:
          colorTheme = 'dark';
      }
      changeState();
    });
  });
}
:root:not([data-theme="dark"]) {
  --bg: #fff;
}
@media (prefers-color-scheme: dark) {
   :root:not([data-theme="light"]) {
    --bg: #000;
  }
}
:root[data-theme="dark"] {
  /* yep, you'll need to duplicate styles from above */
  --bg: #000;
}


body {
  background: var(--bg);
}


.theme-switch:after {
  content: ': system';
}
:root[data-theme="dark"] .theme-switch:after {
  content: ': dark';
}
:root[data-theme="light"] .theme-switch:after {
  content: ': light';
}
<button class="theme-switch">Color scheme</button>

不确定,为什么所有答案都这么复杂。

使用 CSS 变量,像往常一样在媒体查询中设置一个默认值和一个相反的值。还要设置两个 classes 中的值。实现一个切换,在单击时切换这些 classes。

默认情况下,根据系统配色方案使用自动light/dark模式。使用拨动开关切换到手动 light/dark 模式。它 returns 在刷新页面(或从 html 元素中删除 class 后自动进入 light/dark 模式。

// toggle to switch classes between .light and .dark
// if no class is present (initial state), then assume current state based on system color scheme
// if system color scheme is not supported, then assume current state is light
function toggleDarkMode() {
  if (document.documentElement.classList.contains("light")) {
    document.documentElement.classList.remove("light")
    document.documentElement.classList.add("dark")
  } else if (document.documentElement.classList.contains("dark")) {
    document.documentElement.classList.remove("dark")
    document.documentElement.classList.add("light")
  } else {
    if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
      document.documentElement.classList.add("dark")
    } else {
      document.documentElement.classList.add("light")
    }
  }
}
/* automatic/manual light mode */
:root, :root.light {
  --some-value: black;
  --some-other-value: white;
}

/* automatic dark mode */
/* ❗️ keep the rules in sync with the manual dark mode below! */
@media (prefers-color-scheme: dark) {
  :root {
    --some-value: white;
    --some-other-value: black;
  }
}

/* manual dark mode 
/* ❗️ keep the rules in sync with the automatic dark mode above! */
:root.dark {
  --some-value: white;
  --some-other-value: black;
}

/* use the variables */
body {
  color: var(--some-value);
  background-color: var(--some-other-value);
}
<button onClick="toggleDarkMode()">Toggle</button>
<h1>Hello world!</h1>

TL;DR


index.html

<!DOCTYPE html>
<html>
    <head>
        <meta name="color-scheme" content="light dark">
        <link rel="stylesheet" type="text/css" href="style.css" />
    </head>
    <body>
        <h1>Hello world</h1>
        <button id="toggle">Toggle</button>
        <script type="text/javascript" src="script.js"></script>
    </body>
</html>

style.css

.dark-mode {
    background-color: black;
    color: white;
}

.light-mode {
    background-color: white;
    color: black;
}

@media (prefers-color-scheme: dark) {
    body {
        background-color: black;
        color: white;
    }
}

script.js

/**
 * Adopt:
 * the theme from the system preferences; or
 * the previously stored mode from the `localStorage`
 */
var initialMode = "light";
var prefersColorSchemeDark = window.matchMedia( "(prefers-color-scheme: dark)" );

if ( prefersColorSchemeDark.matches ) {
    initialMode = "dark";
}

if( localStorage.getItem("initialMode") == null ) {
    localStorage.setItem("initialMode", initialMode);
}

if( localStorage.getItem("currentMode") == null ) {
    localStorage.setItem("currentMode", initialMode);
} else {
    let currentMode = localStorage.getItem("currentMode");
    if ( currentMode == "dark" && currentMode != initialMode ) {
        document.body.classList.add("dark-mode");
    } else if ( currentMode == "light" && currentMode != initialMode ) {
        document.body.classList.add("light-mode");
    }
}

/**
 * Process the toggle then store to `localStorage`
 */
document.getElementById('toggle').addEventListener("click", function() {
    var initialMode = localStorage.getItem("initialMode");
    let currentMode = localStorage.getItem("currentMode");

    if ( currentMode == "dark" && currentMode == initialMode ) {
        document.body.classList.add("light-mode");
        localStorage.setItem("currentMode", "light");
    } else if ( currentMode == "light" && currentMode == initialMode ) {
        document.body.classList.add("dark-mode");
        localStorage.setItem("currentMode", "dark");
    } else if ( currentMode != initialMode ) {
        document.body.removeAttribute("class");
        if( currentMode == "dark" ) {
            localStorage.setItem("currentMode", "light");
        } else {
            localStorage.setItem("currentMode", "dark");
        }
    }
},
false);

详情

此解决方案假定:

  1. 无论在系统首选项中设置什么(dark/light 模式),都将被确认为初始模式
  2. 从初始模式开始,最终用户可以手动切换暗模式或亮模式
  3. 如果系统没有深色模式功能,将使用浅色模式主题
  4. 无论最终用户先前手动设置的主题(dark/light 模式)是什么,下一页都将是新的初始模式reload/refresh

我建议使用 SCSS。你可以让它更简单。

/* Dark Mode */
@mixin darkMixin {
    body {
        color: #fff; 
        background: #000;
    }
}

@media (prefers-color-scheme: dark) {
    @include darkMixin;
}

.darkMode {
    @include darkMixin;
}

.lightMode {
    body {
        color: #000; 
        background: #fff;
    }
}

并且您可以 toggle/override 使用 JavaScript。 (在此示例中,我使用 jQuery 使其看起来简单)

// dark
$('html').removeClass('lightMode').addClass('darkMode')

// light
$('html').removeClass('darkMode').addClass('lightMode')

如果要检测,这是基于JimmyBanks的代码。

function isDarkTheme(){
    let theme="light";    //default to light
    if (localStorage.getItem("theme")){
        if (localStorage.getItem("theme") == "dark")
            theme = "dark"
    } else if (!window.matchMedia) {
        return false
    } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
        theme = "dark"
    }
    return theme=='dark'
}

要保存当前主题,只需使用localStorage:

localStorage.setItem("theme", 'light')
or
localStorage.setItem("theme", 'dark')

我使用博客 mybyways 找到的替代解决方案,其他任何地方都没有提到,但对我有用。这仅在 html 使用 prefers-color-scheme css 媒体 classes 时有用。

与其他答案不同,它使用样式表的规则附加 class(而不是从 class 列表中添加或删除“深色”或“浅色”)

默认情况下,它采用 OS 设置的样式,并在切换时覆盖它。我从 Google Chrome 实验室尝试过,但对我来说没有成功。

function setPreferredColorScheme(mode = "dark") {
  console.log("changing")
  for (var i = document.styleSheets[0].rules.length - 1; i >= 0; i--) {
    rule = document.styleSheets[0].rules[i].media;
    if (rule.mediaText.includes("prefers-color-scheme")) {
      console.log("includes color scheme")
      switch (mode) {
        case "light":
          console.log("light")
          rule.appendMedium("original-prefers-color-scheme");
          if (rule.mediaText.includes("light")) rule.deleteMedium("(prefers-color-scheme: light)");
          if (rule.mediaText.includes("dark")) rule.deleteMedium("(prefers-color-scheme: dark)");
          break;
        case "dark":
          console.log("dark")
          rule.appendMedium("(prefers-color-scheme: light)");
          rule.appendMedium("(prefers-color-scheme: dark)");
          if (rule.mediaText.includes("original")) rule.deleteMedium("original-prefers-color-scheme");
          break;
        default:
          console.log("default")
          rule.appendMedium("(prefers-color-scheme: dark)");
          if (rule.mediaText.includes("light")) rule.deleteMedium("(prefers-color-scheme: light)");
          if (rule.mediaText.includes("original")) rule.deleteMedium("original-prefers-color-scheme");
      }
      break;
    }
  }
}
@media (prefers-color-scheme: light) {
   :root {
    color: pink;
    background-color: yellow;
  }
}

@media (prefers-color-scheme: dark) {
   :root {
    color: red;
    background-color: blue;
  }
}
<body>

  <button onClick="setPreferredColorScheme()">
    toggle
    </button>
</body>

以上是一个工作示例^

完整来源:https://mybyways.com