在前端开发中,CSS管理和主题切换是构建现代化应用的重要环节。本文将介绍如何在Vite6+React18+Ts项目中实现自定义CSS辅助类以及灵活的主题切换方案。
1.项目CSS架构设计
首先,我们需要建立一个合理的CSS架构。我们采用以下结构:
src
assets
css
├── index.css # 样式入口,引入其它样式,定义tailwind主题theme变量
├── reset.css # 样式重置,由于部分浏览器原生样式表现差异,在这里进行样式归一
├── style.css # 自定义样式,一些常用的自定义样式,如center居中
├── theme.css # 主题变量定义
2.样式重置
reset.css 负责对浏览器原生样式进行重置,抹平浏览器原生样式差异,下文是在 Normalize.css
项目的基础上进行修改的
Normalize.css
项目地址:https://github.com/necolas/normalize.css/
/* +----------------------------------------------------------------------+ */
/* | Reset浏览器样式差异 | */
/* +----------------------------------------------------------------------+ */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body,
html {
margin: 0;
padding: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box;
/* 1 */
height: 0;
/* 1 */
overflow: visible;
/* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
text-decoration: none;
background-color: transparent;
}
a:hover,
a:focus {
text-decoration: none;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none;
/* 1 */
text-decoration: underline;
/* 2 */
text-decoration: underline dotted;
/* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
img,
video {
max-width: 100%;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-size: 100%;
/* 1 */
line-height: 1.15;
/* 1 */
margin: 0;
/* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input {
/* 1 */
overflow: visible;
outline: none;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select {
/* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
appearance: button;
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type='button']::-moz-focus-inner,
[type='reset']::-moz-focus-inner,
[type='submit']::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type='button']:-moz-focusring,
[type='reset']:-moz-focusring,
[type='submit']:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box;
/* 1 */
color: inherit;
/* 2 */
display: table;
/* 1 */
max-width: 100%;
/* 1 */
padding: 0;
/* 3 */
white-space: normal;
/* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type='checkbox'],
[type='radio'] {
box-sizing: border-box;
/* 1 */
padding: 0;
/* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type='number']::-webkit-inner-spin-button,
[type='number']::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type='search'] {
appearance: textfield;
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type='search']::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}
3.自定义CSS辅助类实现
在style.css
文件下,我们创建常用的各种辅助类:
/* +----------------------------------------------------------------------+ */
/* | 自定义CSS辅助类 | */
/* +----------------------------------------------------------------------+ */
.block {
display: block;
clear: both;
}
.hide {
display: none;
}
.show {
display: block;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
4.主题变量定义
在theme.css
文件中定义主题变量,颜色值我们参考了bootstrap5里的文本颜色变量,颜色深度算法采用了 ant design 的色彩算法:
/* +----------------------------------------------------------------------+ */
/* | 自定义Variable主题变量 | */
/* +----------------------------------------------------------------------+ */
/**
* 颜色参考 bootsrap5 中的文本颜色变量,
* 颜色深度,使用ant design色彩算法计算出来的 https://ant.design/docs/spec/colors-cn
*/
:root {
--theme-color-primary-100: #e6f4ff;
--theme-color-primary-200: #b0daff;
--theme-color-primary-300: #87c3ff;
--theme-color-primary-400: #5ea9ff;
--theme-color-primary-500: #368dff;
--theme-color-primary-600: #0d6efd;
--theme-color-primary-700: #004fd6;
--theme-color-primary-800: #003bb0;
--theme-color-primary-900: #00298a;
--theme-color-success-100: #bbc7bf;
--theme-color-success-200: #9bbaa7;
--theme-color-success-300: #74ad8d;
--theme-color-success-400: #52a177;
--theme-color-success-500: #349464;
--theme-color-success-600: #198754;
--theme-color-success-700: #0e613d;
--theme-color-success-800: #053b25;
--theme-color-success-900: #01140d;
--theme-color-info-100: #e6ffff;
--theme-color-info-200: #b0fcff;
--theme-color-info-300: #87f7ff;
--theme-color-info-400: #5eefff;
--theme-color-info-500: #35e2fc;
--theme-color-info-600: #0dcaf0;
--theme-color-info-700: #00a1c9;
--theme-color-info-800: #007da3;
--theme-color-info-900: #005c7d;
--theme-color-warning-100: #fffde6;
--theme-color-warning-200: #fff5ab;
--theme-color-warning-300: #ffec82;
--theme-color-warning-400: #ffe159;
--theme-color-warning-500: #ffd230;
--theme-color-warning-600: #ffc107;
--theme-color-warning-700: #d99b00;
--theme-color-warning-800: #b37a00;
--theme-color-warning-900: #8c5b00;
--theme-color-danger-100: #fff1f0;
--theme-color-danger-200: #ffe1e0;
--theme-color-danger-300: #ffb8b8;
--theme-color-danger-400: #f5898d;
--theme-color-danger-500: #e85d66;
--theme-color-danger-600: #dc3545;
--theme-color-danger-700: #b52236;
--theme-color-danger-800: #8f1428;
--theme-color-danger-900: #69091c;
--theme-color-muted-100: #b1b9bd;
--theme-color-muted-200: #a5acb0;
--theme-color-muted-300: #99a0a3;
--theme-color-muted-400: #8d9396;
--theme-color-muted-500: #81868a;
--theme-color-muted-600: #6c757d;
--theme-color-muted-700: #464e57;
--theme-color-muted-800: #252a30;
--theme-color-muted-900: #07090a;
--theme-color-dark-100: #626669;
--theme-color-dark-200: #565a5c;
--theme-color-dark-300: #4a4d4f;
--theme-color-dark-400: #3e4142;
--theme-color-dark-500: #323436;
--theme-color-dark-600: #212529;
--theme-color-dark-700: #020203;
--theme-color-dark-800: #000000;
--theme-color-dark-900: #000000;
--theme-color-light-100: #f0faff;
--theme-color-light-200: #f0f9ff;
--theme-color-light-300: #f0f9ff;
--theme-color-light-400: #f0f8ff;
--theme-color-light-500: #f0f8ff;
--theme-color-light-600: #f8f9fa;
--theme-color-light-700: #c7cdd4;
--theme-color-light-800: #9aa3ad;
--theme-color-light-900: #727a87;
}
/**
* 其它主题变量在下面定义,这里只列出一种主题下的一种颜色
*/
html.dark {
--theme-color-primary-500: red;
}
5.样式入口
在 index.css 文件中,我们将其它样式文件全部引入进来,并自定义 tailwindcss
的 @theme
参数,以便我们在项目中使用和切换主题。
@import './reset.css';
@import './theme.css';
@import './style.css';
@import 'tailwindcss';
/**
* 去掉这个告警,可以通过vscode 文件-首选项-设置-,输入 css unknow ,将未知的 @ 规则改为 ignore
*/
@theme {
--color-primary-100: var(--theme-color-primary-100);
--color-primary-200: var(--theme-color-primary-200);
--color-primary-300: var(--theme-color-primary-300);
--color-primary-400: var(--theme-color-primary-400);
--color-primary-500: var(--theme-color-primary-500);
--color-primary-600: var(--theme-color-primary-600);
--color-primary-700: var(--theme-color-primary-700);
--color-primary-800: var(--theme-color-primary-800);
--color-primary-900: var(--theme-color-primary-900);
--color-success-100: var(--theme-color-success-100);
--color-success-200: var(--theme-color-success-200);
--color-success-300: var(--theme-color-success-300);
--color-success-400: var(--theme-color-success-400);
--color-success-500: var(--theme-color-success-500);
--color-success-600: var(--theme-color-success-600);
--color-success-700: var(--theme-color-success-700);
--color-success-800: var(--theme-color-success-800);
--color-success-900: var(--theme-color-success-900);
--color-info-100: var(--theme-color-info-100);
--color-info-200: var(--theme-color-info-200);
--color-info-300: var(--theme-color-info-300);
--color-info-400: var(--theme-color-info-400);
--color-info-500: var(--theme-color-info-500);
--color-info-600: var(--theme-color-info-600);
--color-info-700: var(--theme-color-info-700);
--color-info-800: var(--theme-color-info-800);
--color-info-900: var(--theme-color-info-900);
--color-warning-100: var(--theme-color-warning-100);
--color-warning-200: var(--theme-color-warning-200);
--color-warning-300: var(--theme-color-warning-300);
--color-warning-400: var(--theme-color-warning-400);
--color-warning-500: var(--theme-color-warning-500);
--color-warning-600: var(--theme-color-warning-600);
--color-warning-700: var(--theme-color-warning-700);
--color-warning-800: var(--theme-color-warning-800);
--color-warning-900: var(--theme-color-warning-900);
--color-danger-100: var(--theme-color-danger-100);
--color-danger-200: var(--theme-color-danger-200);
--color-danger-300: var(--theme-color-danger-300);
--color-danger-400: var(--theme-color-danger-400);
--color-danger-500: var(--theme-color-danger-500);
--color-danger-600: var(--theme-color-danger-600);
--color-danger-700: var(--theme-color-danger-700);
--color-danger-800: var(--theme-color-danger-800);
--color-danger-900: var(--theme-color-danger-900);
--color-muted-100: var(--theme-color-muted-100);
--color-muted-200: var(--theme-color-muted-200);
--color-muted-300: var(--theme-color-muted-300);
--color-muted-400: var(--theme-color-muted-400);
--color-muted-500: var(--theme-color-muted-500);
--color-muted-600: var(--theme-color-muted-600);
--color-muted-700: var(--theme-color-muted-700);
--color-muted-800: var(--theme-color-muted-800);
--color-muted-900: var(--theme-color-muted-900);
--color-dark-100: var(--theme-color-dark-100);
--color-dark-200: var(--theme-color-dark-200);
--color-dark-300: var(--theme-color-dark-300);
--color-dark-400: var(--theme-color-dark-400);
--color-dark-500: var(--theme-color-dark-500);
--color-dark-600: var(--theme-color-dark-600);
--color-dark-700: var(--theme-color-dark-700);
--color-dark-800: var(--theme-color-dark-800);
--color-dark-900: var(--theme-color-dark-900);
--color-light-100: var(--theme-color-light-100);
--color-light-200: var(--theme-color-light-200);
--color-light-300: var(--theme-color-light-300);
--color-light-400: var(--theme-color-light-400);
--color-light-500: var(--theme-color-light-500);
--color-light-600: var(--theme-color-light-600);
--color-light-700: var(--theme-color-light-700);
--color-light-800: var(--theme-color-light-800);
--color-light-900: var(--theme-color-light-900);
}
6.主题切换实现
通过在html节点修改主题样式类,进而使用不同的主题变量来实现主题切换
import { useState } from 'react';
function App() {
const [theme, setTheme] = useState('light');
const switchTheme = () => {
const el = document.getElementsByTagName('html')[0];
const th = theme != 'dark' ? 'dark' : 'light';
setTheme(th);
el.setAttribute('class', th);
};
return (
<>
<div
style={{
padding: '50px',
}}
>
<button onClick={switchTheme}>切换主题</button>
<p>当前主题:{theme}</p>
<p className="text-primary-500 pt-4">
一大段段落一大段段落一大段段落一大段段一大段段落一大段段落一大段段落一大段段落一大段段落一大段段落
</p>
</div>
</>
);
}
export default App;
当点击 主题切换
按钮时,会调用 switchTheme
方法,进而对html节点的class样式进行修改,当html的class为dark时,生效的主题变量为 html.dark 下的主题变量,即 html.dark 下的主题变量会替换 :root 下的主题变量,从而实现了 text-primary-500
段落内的颜色由 #368dff
转为 red
点击前:

点击后:

7.快速生成主题颜色深度
为了便于快速生成主题颜色深度,基于 ant 颜色算法我们写了一个node脚本,运行 node run/color.js
后可快速生成主题变量
// 生成颜色,控制台终端运行代码: node run/color.js
import { generate } from '@ant-design/colors';
const all = [
/* 重要的文本 rgb: 13,110,253 */
{
key: 'primary',
value: '#0d6efd',
},
/* 执行成功的文本 rgb: 25,135,84 */
{
key: 'success',
value: '#198754',
},
/* 代表一些提示信息的文本 rgb: 13,202,240 */
{
key: 'info',
value: '#0dcaf0',
},
/* 警告文本 rgb: 255,193,7 */
{
key: 'warning',
value: '#ffc107',
},
/* 危险操作文本 rgb: 220,53,69 */
{
key: 'danger',
value: '#dc3545',
},
/* 柔和的文本 */
{
key: 'muted',
value: '#6c757d',
},
/* 深灰色文字 rgb: 33,37,41 */
{
key: 'dark',
value: '#212529',
},
/* 浅灰色文本 rgb: 248,249,250 */
{
key: 'light',
value: '#f8f9fa',
},
];
// 生成tailwind主题颜色
all.forEach((element) => {
const colors = generate(element.value);
for (let i = 0; i < colors.length; i++) {
if (i != 9) {
const item = colors[i];
const numb = (i + 1) * 100;
console.log(
'--color-' +
element.key +
'-' +
numb +
': var(--theme-color-' +
element.key +
'-' +
numb +
');'
);
}
}
});
console.log('-----------------------------------------------');
// 生成css变量
all.forEach((element) => {
const colors = generate(element.value);
for (let i = 0; i < colors.length; i++) {
if (i != 9) {
const item = colors[i];
const numb = (i + 1) * 100;
console.log(
'--theme-color-' + element.key + '-' + numb + ': ' + item + ';'
);
}
}
});