掘友们大家好呀,我是pepedd864。
在看到本文前,你做过的前端主题样式需求有哪些:暗色模式?多颜色主题?根据主题颜色自动延申出沉浸色?修改系统背景图片识别颜色并添加沉浸色?这篇文章将带你了解各种样式切换方案并最终解决我们开头的问题。
在此之前,我写过一篇多主题样式的解决方案,这里相当于从基础到优化,根本解决样式问题。
文章目录思维导图
这里是一些基本的样式切换方案
这个方案是经典的方法,对各种主题的定制也是比较方便的,定制程度是最大化的。
因为它是每一个文件对应一种整体的样式,你在其中可以尽可能修改你网页的细节,不单单是颜色之类的,你还可以设置不同的元素大小,各种特效等等。就像Markdown编辑器的主题方案
它可以用两种方法,分别是
link
的href
引入html<link rel="stylesheet" href="style.css">
@import
指令引入css@import url('light.css')
同时他们还可以这样
html<link rel="stylesheet" media="screen and (prefers-color-scheme: light)" href="./light.css">
<link rel="stylesheet" media="screen and (prefers-color-scheme: dark)" href="./dark.css">
和这样
html<style>
@import url('dark.css') screen and (prefers-color-scheme: dark);
@import url('light.css') screen and (prefers-color-scheme: light);
</style>
通过JS操作link标签的href属性,也可以动态改变引入的样式
例如这样
html<link rel="stylesheet" data-type="css-link" href="./light.css">
<script>
const link = document.querySelector('[data-type="css-link"]');
const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
if (darkQuery.matches) {
link.href = './dark.css';
}
darkQuery.addEventListener('change', e => {
if (e.matches) {
link.href = './dark.css';
} else {
link.href = './light.css';
}
});
</script>
上面的方案中,如果网站对于定制化程度并不是很高
便可以通过类名切换主题
例如上面的,可以改成
css/* light样式主题 */
html.light .box {
color: #000;
background: #fff;
}
/* dark样式主题 */
html.dark .box {
color: #eee;
background: #333;
}
.box {
width: 100px;
height: 100px;
}
或者使用自定义属性data-theme
css/* light样式主题 */
html[data-theme='light'] .box {
color: #000;
background: #fff;
}
/* dark样式主题 */
html[data-theme='dark'] .box {
color: #eee;
background: #333;
}
.box {
width: 100px;
height: 100px;
}
这样可以加快切换速度,但是内容量上来了之后,其实网站的加载会很慢的,而且控制也不好控制,缺点基本都是link方式的缺点。
和上面差不多,只不过操作的是CSS变量,这种方案中,CSS变量可以方便修改,**动态计算(使用calc或者各种CSS内置函数)**等等
例如让GPT生成有哪些CSS内置函数
csscalc(): 用于计算长度值,例如 width: calc(100% - 50px)。
attr(): 返回元素属性的值,例如 content: attr(data-content)。
url(): 用于加载外部资源,例如 background-image: url('image.jpg')。
rgb(), rgba(), hsl(), hsla(): 用于定义颜色值。
linear-gradient(), radial-gradient(): 用于创建渐变背景。
var(): 用于引用CSS自定义属性(变量)的值,例如 color: var(--main-color)。
min(), max(), clamp(): 用于响应式设计,例如 font-size: clamp(1rem, 2vw, 1.5rem)。
repeat(): 用于定义网格布局的重复轨道列表,例如 grid-template-columns: repeat(3, 1fr)。
fit-content(): 用于定义一个元素的尺寸,例如 width: fit-content(20%)。
counter-reset, counter-increment, counter(), counters(): 用于创建或控制计数器。
css:root {
--color: #000;
--background: #fff;
--ele-color: #eee;
--ele-background: #333;
--ele-radius: 5px;
/*为'box'这个元素单独设置一个变量*/
--box-radius-ratio: 1.2
}
body {
color: var(--color);
background: var(--background);
}
html[data-theme='dark']:root {
--color: #eee;
--background: #000;
--ele-color: #333;
--ele-background: #eee;
}
html[data-theme='light']:root {
--color: #000;
--background: #fff;
--ele-color: #eee;
--ele-background: #333;
}
div {
width: 100px;
height: 100px;
color: var(--ele-color);
background: var(--ele-background);
border-radius: var(--ele-radius);
}
.box {
border-radius: calc(var(--ele-radius) * var(--box-radius-ratio));
}
使用原生CSS变量是一种效率相对较高的方式,并且,它可以控制整体样式,因为如果在类名中直接定义属性值,会导致编码时难以统一颜色,大小圆角等等,使用CSS变量就可以将其定义为一种一种的变量,便于管理和控制。
使用CSS变量的另一大好处就是可以使用JS API的CSSStyleDeclaration.setProperty
方法。比如我在:root
中再添加一个--primary-color
变量,那么我可以定义blue
、green
、red
等等几组样式,但是如果我需要能够使用取色盘获取颜色呢,这就需要使用JS去操作CSS变量。
例如soybean的主题控制:
css:root {
...
--primary-color: blue;
...
}
使用setProperty
方法
javascriptdom.style.setProperty(prop, val)
这里主要是如Sass预处理器和TailwindCSS、UnoCSS一类原子化CSS框架的方案
这里主要是和[1.2 使用类名或者自定义属性切换](##1.2 使用类名或者自定义属性切换)差不多。但是使用了Sass简化的代码书写(并没有减少代码量,甚至代码量非常大,会带来很大的性能消耗)。
比如下面这段代码
scssbody {
position: relative;
transition: background-color 0.3s,
color 0.3s;
html[data-dark='light'][data-theme='red'] & {
background-color: red;
color: #000;
}
}
当data-dark='light'
和data-theme='red'
同时成立时,背景颜色设置成红色
那如果我们需要设置多组主题和模式时,代码就会变成下面这个样子
scssbody {
position: relative;
transition: background-color 0.3s,
color 0.3s;
html[data-dark='light'][data-theme='red'] & {
background-color: red;
color: #000;
}
html[data-dark='light'][data-theme='orange'] & {
background-color: orange;
color: #000;
}
html[data-dark='light'][data-theme='yellow'] & {
background-color: yellow;
color: #000;
}
html[data-dark='light'][data-theme='cyan'] & {
background-color: cyan;
color: #000;
}
...
}
因此,我们需要使用sass来帮我们减少重复的代码,使用@mixin
和@each
可以批量生成上面重复的片段
scss$modes: (
light: (
bgColor: #fff,
infoColor: #000
),
dark: (
bgColor: #000,
infoColor: #fff
)
);
@mixin useTheme() {
@each $key, $value in $modes {
html[data-dark='#{$key}'] & {
@content;
}
}
}
下面这段代码
scssbody {
position: relative;
transition: background-color 0.3s,
color 0.3s;
@include useTheme {
}
}
相当于
scssbody {
position: relative;
transition: background-color 0.3s,
color 0.3s;
html[data-dark='light'] & {
}
html[data-dark='dark'] & {
}
...
}
同样的,因为我们使用了主题色和亮暗模式的组合,所以也需要使用到两层@each
循环遍历
scss@mixin useTheme() {
@each $key1, $value1 in $modes {
@each $key2, $value2 in $colors {
html[data-dark='#{$key1}'][data-theme='#{$key2}'] & {
@content;
}
}
}
}
接下来,就是根据当前的主题和模式返回对应的颜色了,这里我们需要一个全局变量存储当前的颜色和模式
scss$curMode: light;
$curTheme: red;
@mixin useTheme() {
@each $key1, $value1 in $modes {
$curMode: $key1 !global;
@each $key2, $value2 in $colors {
$curTheme: $key2 !global;
html[data-dark='#{$key1}'][data-theme='#{$key2}'] & {
@content;
}
}
}
}
并完成颜色的定义
scss$colors: (
red: (
primary: $red,
info: $red,
),
orange: (
primary: $orange,
info: $orange,
),
yellow: (
primary: $yellow,
info: $yellow,
),
cyan: (
primary: $cyan,
info: $cyan,
),
green: (
primary: $green,
info: $green,
),
blue: (
primary: $blue,
info: $blue,
),
purple: (
primary: $purple,
info: $purple,
)
);
然后,写一个根据$curMode
和$curTheme
返回对应值的函数
scss@function getModeVar($key) {
$modeMap: map-get($modes, $curMode);
@return map-get($modeMap, $key);
}
@function getColor($key) {
$themeMap: map-get($colors, $curTheme);
@return map-get($themeMap, $key);
}
然后在混合里面就可以使用函数获取当前主题或模式对应的颜色值了
scssbody {
position: relative;
transition: background-color 0.3s,
color 0.3s;
@include useTheme {
background-color: getModeVar('bgColor');
color: getModeVar('infoColor');
}
}
使用theme.scss
scss<template>
<div class="test">test</div>
</template>
<style lang="scss" scoped>
@import "@/styles/theme";
.test {
@include useTheme {
background: getColor('primary');
}
}
</style>
如果大家理解了这种方法的话,其实会觉得这种方法写代码确实比较方便,因为Sass内置丰富的如变量、循环、分支、函数等等可以让我们写出的样式十分灵活且简洁。
比如在我的项目中,使用Sass的内置函数方便的提取出颜色的色相进行转换得到各种沉浸色。
代码像这样
再讲为什么他会导致代码量非常大,并且性能损耗很大。
因为使用的是双层循环生成,时间复杂度达到了,不过预处理器会在代码编译阶段生成对应的CSS代码,它可能会导致编译时间增加一点,但是编译工具的效率一般都非常高,不会受这个影响,除非你的循环层数非常多,代码中使用的地方非常多,那应该是你的内存先承受不住了。
像这样的代码
scssbody {
@include useTheme {
background-color: getModeVar('bgColor');
color: getModeVar('infoColor');
}
}
他会编译成个差不多的代码,一份代码能解决的事情,变成了16份,CSS文件的大小会变得很大。
这里我就讲Tailwind CSS,没用过UnoCSS
TailwindCSS只需一个dark
指令便可完成跟随系统主题切换
cssbody {
@apply dark:bg-gray-800 bg-gray-50;
}
当然也支持手动操作,更多可查看Tailwind CSS 文档 Dark Mode - Tailwind CSS
它支持一个theme
函数,它可以像这样使用
cssbody {
background: theme(colors.primary);
}
在tailwind.config.js
定义值即可
jsexport default {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
colors: {
'primary': '#f500ff',
},
},
corePlugins: {
preflight: false,
},
plugins: [],
}
但是要注意在sass中的使用,它应该是这样的
scssbody {
background: #{'theme(colors.primary)'};
}
更多的以后再更新 TODO
html<script setup>
import {useAppStore} from "@/stores/app.js"
import variables from '@/styles/variables.module.scss'
import {ref} from "vue";
const app = useAppStore()
const backgroundColor = ref('')
function toggleColor(color) {
console.log(color)
backgroundColor.value = color
}
</script>
<template>
<a-config-provider :theme="app.themeConfig">
<a-select v-model:value="app.themeName" style="width: 240px">
<a-select-option v-for="(color, name) in variables" :value="name"> {{ name }}:{{ color }}</a-select-option>
</a-select>
<a-button-group>
<a-button @click="toggleColor(variables[app.themeName])">切换主题- {{ app.themeName }}</a-button>
</a-button-group>
<div class="test">test</div>
</a-config-provider>
</template>
<style lang="scss" scoped>
@import "@/styles/theme.scss";
.test {
background: v-bind(backgroundColor);
}
</style>
使用的就是Vue3 的 v-bind这个指令
底层也是使用CSSStyleDeclaration.setProperty
实现的,这里带上了Vue scoped的样式隔离,计算了一个独一无二的哈希值 。
CSS in JS是一种方案,Vue也能用,不过大多数都是在React中使用的。
例如
jsximport {useState} from 'react';
import styled from 'styled-components';
const StyledButton = styled.button`
background-color: ${props => props.color};
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
`;
function App() {
const [color, setColor] = useState('blue');
const handleClick = () => {
setColor(prevColor => prevColor === 'blue' ? 'green' : 'blue');
};
return (
<StyledButton color={color} onClick={handleClick}>
Click me
</StyledButton>
);
}
export default App;
它的话,几乎就是纯JS了,那么你可以定义一个全局的样式变量来控制组件的样式,例如Ant Design Vue就是使用CSS in JS方案实现的主题。
如果只是单纯提取图片的主要颜色,其实非常简单,只需要使用 canvas 获取图片数据,然后统计出现最多的颜色即可
jsimg.src = './test/assets/droplets-7401411_1280.jpg'
// img.src = 'https://cdn.pixabay.com/photo/2024/11/26/18/50/skyscraper-9226515_1280.jpg'
img.crossOrigin = 'anonymous'
img.onload = function (e) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0)
const imageData = ctx.getImageData(0, 0, img.width, img.height)
const data = imageData.data
const colorCount = {}
let maxCount = 0
let dominantColor = ''
// 遍历每个像素
for (let i = 0; i < data.length; i += 4) {
const r = data[i]
const g = data[i + 1]
const b = data[i + 2]
const alpha = data[i + 3]
// 忽略完全透明的像素
if (alpha === 0) continue
const color = `rgb(${r},${g},${b})`
// 记录颜色出现次数
if (color in colorCount) {
colorCount[color]++
} else {
colorCount[color] = 1
}
// 更新最大出现次数和对应颜色
if (colorCount[color] > maxCount) {
maxCount = colorCount[color]
dominantColor = color
}
}
console.log(`Dominant Color: ${dominantColor}`)
document.body.style.backgroundColor = dominantColor
}
console.log(img)
实际的开发中,比如根据图片得出一组 pattle 调色盘,调色盘的颜色是图片中主要主体的颜色,可以使用这个库 color-thief
,同时,也有一些算法可以实现提色的效果,比如这个博文的方法:https://segmentfault.com/a/1190000041438074
首先,我们需要明白一点,在真实的项目中,不可能只用CSS就能实现完整的复杂样式操作;也不会用JS去进行消耗性能的CSS操作。更多是时候是,CSS负责样式,JS负责逻辑,就像我们学习这两个语言时一样。
我们这里参考Soybean的主题实现方案,在他的官方文档中也给出了,我这里实现一个简单版本的主题控制。系统主题 | SoybeanAdmin (soybeanjs.cn)
第一步就是生成一系列颜色调色盘,这样我们便可以根据一组颜色去设计网页整体风格
更高级的可能还会加入二级颜色,像这样
不过我们这里实现一级颜色即可。
实现的效果像这个示例网址给出的一样。uicolors.app/create
我们可以观察到,它是由一个主颜色生成了11个颜色,其中“锁”所在的位置便是主颜色,并且各个颜色块上的字体都是适配背景的暗亮色和根据背景生成的沉浸色。
同时它支持暗亮色模式,外部字体和body的背景会跟着模式切换而变化,但是字体是常规的颜色。
分析思路
我们需要使用HSL转换颜色
什么是HSL:
H即色相:8bit颜色可分为256份不同的颜色,使用环状图像表示。
S即饱和度:代表颜色的鲜艳程度,S越低,颜色越偏向灰色,S越高,颜色的灰度会减小。
L即亮度:代表颜色是更偏向于白色还是黑色,L越高,颜色越亮。
这里我们需要使用一个colord库,方便我们进行颜色的转化。
例如我们使用
jsconst color = '#1890ff'
console.log(getHsl(color));
得到
{h: 209, s: 100, l: 55, a: 1}
我们根据生成的规则,可以写出如下代码
tsimport {getHex, getHsl} from "./theme/utils/colord.ts";
import {colord} from "colord";
const color = '#ff0000'
// 调色盘数值数组
const colorPaletteNumbers = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]
const len = colorPaletteNumbers.length
// 生成的颜色数组
let colorPalette = []
const min = 10 // 亮度最小值
const max = 90 // 亮度最大值
// 每个部分之间相隔的亮度
const part = ((max - min) / 11) // 这个用于定位
// 计算索引
const hslC = getHsl(color)
if (hslC.l < min) hslC.l = min
if (hslC.l > max) hslC.l = max
console.log(part, (len - Math.floor((hslC.l - min) / part)) - 1)
const index = (len - Math.floor((hslC.l - min) / part)) - 1
// 添加到调色盘中
colorPalette[index] = {
num: colorPaletteNumbers[index],
color: getHex(hslC)
}
// 根据当前值计算其他值
for (let i = 0; i < len; i++) {
if (i === index) {
continue
}
const diff = i - index
const newL = hslC.l - (diff * part)
const newColor = colord({...hslC, l: Math.floor(newL)}).toHex()
console.log(i, diff, newL, colorPaletteNumbers[i], getHsl(newColor))
colorPalette[i] = {
num: colorPaletteNumbers[i],
color: newColor
}
}
document.querySelector<HTMLElement>('#app')!.innerHTML = `
<div class="container">
${colorPalette.map((item, idx) =>
`<div class="box" style="background:${item.color};">
${item.num}${idx === index ? '✨' : ''}<br/>${item.color}
</div>`
).join('')
}
</div>
`
效果是这样的,还不错,基本还原了他的效果(其实它这里对颜色的色相还进行了一些调整,他这里的算法并不只是简单根据亮度进行分段,比较复杂,我这里只实现了简单的),你会发现,颜色深的地方的字体根本没法看啊,于是,我们需要根据背景生成对应的字体颜色。
只需要将亮度大于50的颜色混合一个黑色便可得到字体颜色,同理小于50的颜色混合白色
ts// 计算字体颜色
const midColor = colorPalette[Math.floor(len / 2)].color
colorPalette = colorPalette.map(item => {
const color = colord(item.color)
const text = mixColor(color.toHsl().l < 50 ? '#ffffff' : '#000000', midColor, 0.5)
return {
...item,
text,
}
})
document.querySelector<HTMLElement>('#app')!.innerHTML = `
<div class="container">
${colorPalette.map((item: any, idx: any) =>
`<div class="box" style="background:${item.color};color:${item.text}">
${item.num}${idx === index ? '✨' : ''}<br/>${item.color}
</div>`
).join('')
}
</div>
`
做到这里,你会发现,这有什么用呢,我要的是动态主题啊,你这是什么
而下面我就要讲到,如何使用这一关键的调色盘
在上文中,我们得到了colorPalette
数组,根据这个数组,我们只需要将其放在CSS变量中,并使用类名进行管理,便可以很轻松地实现动态主题。
首先是生成CSS变量,并将其放在style
标签中
tsfunction addPaletteToHTML(palette) {
function getCssVarStr(arr) {
const cssVarArr = arr.map(item => {
const cssVarPrimary = `--primary-${item.num}-color:${item.color};\n`
const cssVarImmersiveText = `--immersive-text-${item.num}-color:${item.text};\n`
return cssVarPrimary + cssVarImmersiveText
})
return cssVarArr.join('')
}
const cssVarStr = getCssVarStr(palette)
const styleId = 'palette-colors'
const style = document.querySelector(`#${styleId}`) || document.createElement('style');
style.id = styleId
// 插入生成的调色盘颜色CSS变量
style.innerHTML = `
html {
${cssVarStr}
}
`
document.head.appendChild(style);
}
像这样
然后生成类名,我们便可以直接使用类名了
tsfunction addThemeClassToHTML() {
function getThemeClass() {
const colorPaletteNumbers = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]
const classStr = colorPaletteNumbers.map(num => {
return `.bg-primary-${num} {background-color:var(--primary-${num}-color);color:var(--immersive-text-${num}-color)}\n`
})
return classStr.join(' ')
}
const classStr = getThemeClass()
console.log(classStr)
const styleId = 'theme-class'
const style = document.querySelector(`#${styleId}`) || document.createElement('style');
style.id = styleId
style.innerHTML = `${classStr}`
document.head.appendChild(style)
}
像这样,你会觉得看起来很像原子化CSS的写法,所以之后在框架中,我们将使用 TailwindCSS 管理
tsconst colorPaletteNumbers = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]
document.querySelector<HTMLElement>('#app')!.innerHTML = `
<div class="container">
${colorPaletteNumbers.map((item: any) =>
`<div class="box bg-primary-${item}">
${item}
</div>`
).join('')
}
</div>
`
但是,如何才能直观地看到它是动态的呢,比如这个例子
tsconst setting = new Proxy({
color: '#1890ff'
}, {
set: function (target, property, value) {
if (property === 'color') {
target[property] = value;
const colorPalette = generatePalette(value)
addPaletteToHTML(colorPalette)
return true
}
target[property] = value;
return true
}
})
`<input type="color" id="color-picker" value="#1890ff">`
const colorPicker = document.querySelector<HTMLInputElement>('#color-picker')!;
colorPicker.addEventListener('input', function () {
setting.color = this.value;
});
这里主要通过类名控制CSS变量,实现暗色模式,详细可看[1.3.1 静态切换](####1.3.1 静态切换)
tsfunction addPaletteToHTML(palette) {
function getCssVarStr(arr) {
const cssVarArr = arr.map(item => {
const cssVarPrimary = `--primary-${item.num}-color:${item.color};\n`
const cssVarImmersiveText = `--immersive-text-${item.num}-color:${item.text};\n`
return cssVarPrimary + cssVarImmersiveText
})
return cssVarArr.join('')
}
const cssVarStr = getCssVarStr(palette)
const innerHTML = `
html {
${cssVarStr}
--background-color: #fff;
--text-color: #000;
}
html.dark {
${cssVarStr}
--background-color: #1C1C1CFF;
--text-color: #fff;
}
`
updateStyleToEle('palette-colors', innerHTML)
}
const setting = new Proxy({
color: '#1890ff',
darkMode: true
}, {
set: function (target, property, value) {
target[property] = value;
if (property === 'color') {
const colorPalette = generatePalette(value)
addPaletteToHTML(colorPalette)
return true
}
if (property === 'darkMode') {
document.documentElement.classList.toggle('dark', value);
}
return true
}
})
`
<div>
<label for="dark-mode-switch">暗色模式</label>
<input type="radio" id="dark-mode-switch" name="mode">
<label for="light-mode-switch">亮色模式</label>
<input type="radio" id="light-mode-switch" name="mode">
</div>
`
const modeSwitches = document.querySelectorAll<HTMLInputElement>('input[name="mode"]');
modeSwitches.forEach(switchElement => {
switchElement.addEventListener('change', function () {
// 当用户切换模式时,更新themeSetting.darkMode
setting.darkMode = this.id === 'dark-mode-switch';
});
});
在css中便可以直接使用变量
cssbody {
display: flex;
justify-content: center;
align-items: center;
background: var(--background-color);
color: var(--text-color);
height: 100vh;
transition: all 0.3s;
}
将代码拆分,放到对应目录下,使用npm包模板(这边是我自己做的一个)打包发布即可。
然后在框架使用
npm install @healwrap/hp-theme
引入依赖使用即可
tsimport { initTheme, themeSetting } from '@healwrap/hp-theme'
initTheme(themeSetting.colorConfig)
...
参考链接
代码
本文作者:pepedd864
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!