yliu
yliu
时来天地皆同力,运去英雄不自由
  • GitHubGitHub
  • 掘金掘金
  • 知乎知乎
总访问量 loading...
页面设置RSS 订阅

现代 CSS 切换主题方式探索

css 相关loading...次阅读

sgare-icon
sgare-icon
sgare-icon

最近重构了博客系统,其中涉及到主题切换的方案,于是就有了这次的分享。

示例图片

UnoCSS 方案

使用 CSS 变量配合 UnoCSS 插件注入的方式完成主题切换功能。

具体来说,我们的主题有三种模式:

  1. 跟随系统;
  2. 浅色;
  3. 深色。

其中,用户手动选择的模式会覆盖系统跟随模式。

一个比较容易想到的方案是,在 :root 中定义一些 CSS 变量,例如:

:root {
  --color: red;
  --dark-color: #333;
}

但这样使用起来会有些麻烦,因为 UnoCSS 的使用方式主要是通过类名定义。

例如:

<div class="bg-[var(--color)] bg-[var(--dark-color)]">内容</div>

这样会导致两个变量同时存在的问题,同时也会丧失 UnoCSS 插件的提示功能。

针对这两个问题,可以通过类名覆盖的方式改进方案:

:root {
  --color: red;
}
html.dark {
  --color: #333;
}
<div class="bg-[var(--color)]">内容</div>

这样就可以通过 --color 根据主题切换颜色。不过,这个方案仍然存在一些不足之处:

  1. 缺少编辑器提示;
  2. 前缀 bg-[var(--color)] 过长。

使用 Theme 配置

UnoCSS 在 Theme 中提供了一个代码示例:

theme: {
  colors: {
    veryCool: '#0000ff', // class="text-very-cool"
    brand: {
      primary: 'hsl(var(--hue, 217) 78% 51%)', // class="bg-brand-primary"
      DEFAULT: '#942192', // class="bg-brand"
    },
  },
}

通过扩展 colors,可以直接引用变量。例如:

theme: {
  colors: {
    color: 'var(--color)',
  },
}

然后结合如下 CSS 和 HTML:

<style>
  :root {
    --color: red;
  }
  html.dark {
    --color: #333;
  }
</style>
<div class="bg-color">内容</div>

这样解决了两个问题:

  1. 缩短了前缀;
  2. 提供了编辑器提示。

不过,这仍需一个单独的 CSS 文件来管理变量。是否有更好的方式来管理这些变量呢?

使用 Presets

观察以下 style 内容:

:root {
  --color: red;
}
html.dark {
  --color: #333;
}

我们可以通过 UnoCSS 的 Presets 注入这些信息。Presets 是 UnoCSS 的配置项,最终会合并到配置文件中。

例如,官方在 Preflights 中的一个示例:

preflights: [
  {
    getCSS: ({ theme }) => `
      * {
        color: ${theme.colors.gray?.[700] ?? "#333"};
        padding: 0;
        margin: 0;
      }
    `,
  },
];

根据这个思路,我们可以定义一个 Preset,将 CSS 注入到页面中。例如:

import { themePreset } from "./plugins/theme-preset";
import { defineConfig, presetUno } from "unocss";

export default defineConfig({
  presets: [
    themePreset({
      primary: {
        light: "#0F7AE5",
        dark: "#2B8EF0",
      },
      code: "#333",
    }),
  ],
});

生成的 CSS 如下:

:root {
  --code: #333;
  --primary: #0f7ae5;
}
html.dark {
  --primary: #2b8ef0;
}

使用方式保持不变,变量注入的工作由 UnoCSS 自动完成。

theme-preset.js 的部分示例:

{
    theme: {
      colors: {
       code: 'var(--code)',
       primary: 'var(--primary)',
      },
    },
preflights: [
  {
    getCSS: () => `
:root {
  --code: #333;
  --primary: #0F7AE5;
}
html.dark {
    --primary: #2B8EF0;
}
    `,
  },
]
}

使用 light-dark 函数

今天阅读了一篇文章:CSS: Easy dark mode。文章介绍了一种新思路,通过 CSS 函数实现主题切换,无需依赖媒体查询。

示例代码:

:root {
  color-scheme: light dark;
}
body {
  color: light-dark(#333b3c, #efefec);
  background-color: light-dark(#efedea, #223a2c);
}

注意:color-scheme 是必须的。

不过 light-dark 兼容性还存在一些问题:

兼容性问题示例

虽然现代浏览器支持良好,但若考虑兼容性,可以配合 postcss-dark-theme-class 使用。

安装插件:

npm install --save-dev postcss postcss-dark-theme-class

配置文件 postcss.config.cjs

module.exports = {
  plugins: [require("postcss-dark-theme-class")],
};

样式文件 style.css

#app {
  color: light-dark(black, white);
}

生成的 CSS 如下:

@media (prefers-color-scheme: dark) {
  :where(html:not(.is-light)) #app {
    color: white;
  }
}
:where(html.is-dark) #app {
  color: white;
}
@media (prefers-color-scheme: light) {
  :where(html:not(.is-dark)) #app {
    color: black;
  }
}
:where(html.is-light) #app {
  color: black;
}

这种方式生成的代码和 UnoCSS 类似,但增加了 :where:not 的规则。

:where 是一个没有权重的选择器,:not 就是排除规则生效。

总结

对比两种方案:

  • UnoCSS:需要结合工具实现,若切换主题的配置项较多,可能会显得繁琐。
  • light-dark:写法更简单,但生成的 CSS 文件较大,且需注意兼容性问题。

每种方案都有其适用场景,可以根据实际需求选择。

如果考虑使用 UnoCss 方案需要查看完整代码,可以看下我的仓库文件

feedback错误反馈

☕️
请我喝一杯咖啡