ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

彩彩与千圣:基于CSS自定义属性的水蜜桃主题系统实战

彩彩与千圣:基于CSS自定义属性的水蜜桃主题系统实战 1. 背景为什么需要一个统一的“水蜜桃色”主题体系1.1 从一次视觉改版说起最近团队接到一个新的前端项目需求为一个轻社交类 Web 应用重新设计视觉风格希望整体走温柔、明亮、带有水果般通透感的路线。产品经理给到的关键词很浪漫——“水蜜桃恋色物语”。这个需求最初看起来只涉及“换个颜色”但真正进入开发后才发现事情远没有想象中那么简单。页面上有按钮、卡片、表单、标签、表格、弹窗、提示消息、导航栏几乎所有组件都会使用颜色。如果每个人各自从设计稿里取色最后一定会出现“同一个按钮在不同页面颜色不一致”“暗色模式下文字看不清”“某个组件的 hover 态没有定义”之类的问题。于是我们决定把这次视觉升级做成一套可维护的前端主题系统项目代号就叫“彩彩与千圣”。1.2 项目代号的含义看到这个标题你可能会觉得它不像一个技术项目名。其实这是我们内部的两个模块取名彩彩负责色彩设计令牌Design Token的定义包括原始色板、语义色板、状态色、间距、圆角、阴影等基础变量。千圣负责组件样式层Component Style它消费彩彩的色彩令牌把按钮、输入框、卡片等组件的最终视觉效果稳定下来。简单来说“彩彩”解决的是“有哪些颜色可用”“千圣”解决的是“这些颜色怎么被安全地使用”。两者配合才算完成一套真正可落地的主题系统。1.3 解决的核心问题这套系统主要解决四类问题色彩统一所有组件从同一套 CSS 自定义属性中取值避免人工取色不一致。主题切换支持亮色与暗色两种模式通过>npm init -y生成的基础package.json大概长这样{ name: peach-theme-demo, version: 1.0.0, description: 彩彩与千圣水蜜桃恋色物语主题系统示例, main: index.js, scripts: { start: npx serve . }, keywords: [css, design-token, theme], license: MIT }后续如果需要引入 Sass、PostCSS、Vite 或 Webpack都可以通过这个文件统一管理。本文为了让示例足够轻量暂时不引入构建链直接用浏览器打开或使用npx serve启动静态服务。3. Design Token 与色彩体系设计3.1 什么叫 Design TokenDesign Token 翻译过来是“设计令牌”它可以理解为设计系统里最小的视觉属性单元。一个颜色、一个圆角、一个间距、一个阴影都可以是一个 Token。在代码层面CSS 自定义属性CSS Variables是承载 Token 很自然的方式。例如:root { --color-primary: #f8a5a5; }这样定义之后整个项目都可以通过var(--color-primary)引用这个颜色。如果将来品牌色调整只需要改一个地方。设计令牌最大的价值不是“省几个变量”而是让视觉表达有了语义。比如--color-danger不要直接写“红色”而是告诉开发者“这个颜色用于危险操作场景”具体是偏粉的红、偏暗的红由设计侧决定。3.2 水蜜桃色系的色彩分层“水蜜桃恋色物语”想要传递的感觉是柔软、清新、微甜、不刺眼。如果直接把纯粉色堆满页面视觉上会非常疲劳因此需要分层设计。我们把颜色拆成几个层级品牌主色页面最核心的强调色水蜜桃的果肉感常用于主按钮、链接、选中态。辅助色用于信息提示、成功、警告、危险等状态。中性色文字、边框、背景、分隔线等不是“没有颜色”而是饱和度极低的色彩。表面色页面背景、卡片背景、浮层背景需要区分层级关系。叠加态hover、active、focus、disabled 使用的临时状态。这些层级之间要保证对比度。比如主色用在按钮背景时按钮上的文字颜色需要足够深或足够白避免看不清。3.3 HSL 与十六进制颜色怎么选开发中经常使用十六进制颜色比如#FFB6C1。十六进制直观但有一个问题它的调整不够直觉。如果你觉得某个颜色“太亮了”在十六进制里要想一会儿才知道改哪一位。HSL 则更接近人对颜色的感知H色相Hue0-360粉色大概在 340 到 360 之间。S饱和度Saturation百分比越高越鲜艳。L亮度Lightness百分比越低越暗。例如水蜜桃色可以表达为:root { --peach-h: 15; --peach-s: 80%; --peach-l: 75%; }这样调整亮度只需要改--peach-l其他属性保持不变。在 CSS 中使用 HSL 还可以配合calc()比如 hover 时亮度降低.btn-primary:hover { background-color: hsl(var(--peach-h) var(--peach-s) calc(var(--peach-l) - 8%)); }这个示例展示了“色彩计算”的灵活性。不过要注意这种写法对浏览器新特性有要求如果项目需要兼容较老浏览器建议直接用静态色值或者在构建阶段用 PostCSS 处理。3.4 语义化命名规范命名规范是整个彩彩模块的核心。我们采用这样的约定--color-{类型}-{场景}类型primary主色、success成功、warning警告、danger危险、info信息、neutral中性色。场景default默认态、hover悬停态、active激活态、disabled禁用态、bg背景、border边框、text文字。例如--color-primary-default: #f9b5ac; --color-primary-hover: #f6a09b; --color-primary-active: #e9908c; --color-primary-disabled: #fbd2cf; --color-primary-text: #5e2b2b;这里虽然变量变多了但每个变量都有明确用途。组件层不会再出现“按钮背景是#f9b5ac”这种写死颜色而是写“按钮背景是var(--color-primary-default)”。需要提醒的是命名不是越细越好。变量太多会导致维护负担变量太少又会让样式层继续写魔法值。通常一个项目 40 到 80 个颜色变量是比较合理的范围具体看业务复杂度。4. 实战构建彩彩与千圣主题系统4.1 编写基础色彩令牌彩彩模块文件路径css/variables.css/* * 彩彩模块 * 负责定义水蜜桃主题下的设计令牌 * 所有颜色变量统一维护在 :root 中 */ :root { color-scheme: light; /* 色相基准 */ --peach-h: 12; --peach-s: 88%; --peach-l: 78%; /* 品牌主色水蜜桃果肉色 */ --color-primary-default: hsl(var(--peach-h) var(--peach-s) var(--peach-l)); --color-primary-hover: hsl(var(--peach-h) var(--peach-s) calc(var(--peach-l) - 7%)); --color-primary-active: hsl(var(--peach-h) var(--peach-s) calc(var(--peach-l) - 12%)); --color-primary-disabled: hsl(var(--peach-h) 60% 88%); --color-primary-text: #642a2a; /* 辅助色 */ --color-success: #6fbf8f; --color-warning: #e6b566; --color-danger: #e5817a; --color-info: #6fa8dc; /* 中性色文字层级 */ --color-text-primary: #3b2b2b; --color-text-regular: #5f5151; --color-text-secondary: #8d7d7d; --color-text-disabled: #bfb1b1; /* 中性色边框与分割线 */ --color-border: #f0dcd8; --color-border-light: #f7e9e6; --color-divider: #f5e3e0; /* 表面色背景层级 */ --color-bg-page: #fff7f4; --color-bg-container: #ffffff; --color-bg-elevated: #ffffff; --color-bg-mask: rgba(90, 40, 40, 0.4); /* 形状与阴影 */ --radius-sm: 6px; --radius-md: 12px; --radius-lg: 20px; --shadow-card: 0 2px 12px rgba(180, 120, 110, 0.08); --shadow-float: 0 8px 24px rgba(180, 120, 110, 0.14); /* 动效 */ --transition-fast: 0.2s ease; --transition-base: 0.3s ease; } /* * 暗色主题 * 当 html 标签带有>/* * 基础样式 * 负责页面级背景、文字、过渡效果 */ * { box-sizing: border-box; margin: 0; padding: 0; } html, body { height: 100%; } body { font-family: PingFang SC, Hiragino Sans GB, Microsoft YaHei, sans-serif; background-color: var(--color-bg-page); color: var(--color-text-primary); line-height: 1.6; transition: background-color var(--transition-base), color var(--transition-base); } a { color: var(--color-primary-default); text-decoration: none; transition: color var(--transition-fast); } a:hover { color: var(--color-primary-hover); } /* 页面容器 */ .container { max-width: 960px; margin: 0 auto; padding: 32px 20px 64px; } /* 页面顶部标题 */ .page-header { margin-bottom: 40px; padding-bottom: 20px; border-bottom: 1px solid var(--color-divider); } .page-header h1 { font-size: 28px; font-weight: 700; display: flex; align-items: center; gap: 12px; } .page-header .subtitle { margin-top: 8px; color: var(--color-text-secondary); font-size: 15px; }这里需要留意body上的transition让主题切换有一点过渡动画避免瞬间变化带来的生硬感。不过这种全局过渡对性能没有太大负担因为只有背景色和文字色发生变化。4.3 编写组件样式千圣模块文件路径css/components.css这一部分由“千圣”模块维护它负责把彩彩提供的变量应用到真实组件上。/* * 千圣模块 * 组件样式层只引用 variables.css 中的设计令牌 */ /* 按钮 */ .btn { display: inline-flex; align-items: center; justify-content: center; padding: 8px 20px; border-radius: var(--radius-md); border: 1px solid transparent; font-size: 14px; font-weight: 500; cursor: pointer; transition: background-color var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast), box-shadow var(--transition-fast); } /* 主按钮 */ .btn-primary { background-color: var(--color-primary-default); color: var(--color-primary-text); } .btn-primary:hover { background-color: var(--color-primary-hover); } .btn-primary:active { background-color: var(--color-primary-active); } .btn-primary:disabled { background-color: var(--color-primary-disabled); color: var(--color-text-disabled); cursor: not-allowed; } /* 默认按钮 */ .btn-default { background-color: var(--color-bg-container); color: var(--color-text-regular); border-color: var(--color-border); } .btn-default:hover { border-color: var(--color-primary-default); color: var(--color-primary-default); } /* 危险按钮 */ .btn-danger { background-color: var(--color-danger); color: #fff; } .btn-danger:hover { filter: brightness(0.94); } /* 卡片 */ .card { background-color: var(--color-bg-container); border-radius: var(--radius-lg); box-shadow: var(--shadow-card); padding: 24px; transition: background-color var(--transition-base), box-shadow var(--transition-base); } .card .card { margin-top: 16px; } .card-title { font-size: 18px; font-weight: 600; margin-bottom: 12px; } /* 标签 Tag */ .tag { display: inline-flex; align-items: center; padding: 4px 12px; border-radius: 999px; font-size: 13px; line-height: 1.4; background-color: rgba(255, 255, 255, 0.3); border: 1px solid var(--color-border); color: var(--color-text-regular); } .tag-success { color: var(--color-success); border-color: var(--color-success); background-color: color-mix(in srgb, var(--color-success) 12%, transparent); } .tag-warning { color: var(--color-warning); border-color: var(--color-warning); background-color: color-mix(in srgb, var(--color-warning) 12%, transparent); } .tag-danger { color: var(--color-danger); border-color: var(--color-danger); background-color: color-mix(in srgb, var(--color-danger) 12%, transparent); } /* 输入框 */ .input { width: 100%; padding: 10px 14px; border: 1px solid var(--color-border); border-radius: var(--radius-md); background-color: var(--color-bg-container); color: var(--color-text-primary); font-size: 14px; transition: border-color var(--transition-fast), box-shadow var(--transition-fast); outline: none; } .input::placeholder { color: var(--color-text-disabled); } .input:focus { border-color: var(--color-primary-default); box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary-default) 20%, transparent); } /* 表单分组 */ .form-item { margin-bottom: 16px; } .form-item label { display: block; font-size: 14px; font-weight: 500; margin-bottom: 6px; color: var(--color-text-regular); } /* 提示框 */ .alert { padding: 12px 16px; border-radius: var(--radius-md); font-size: 14px; display: flex; align-items: flex-start; gap: 8px; } .alert-info { background-color: color-mix(in srgb, var(--color-info) 12%, transparent); color: var(--color-info); } .alert-success { background-color: color-mix(in srgb, var(--color-success) 12%, transparent); color: var(--color-success); } .alert-warning { background-color: color-mix(in srgb, var(--color-warning) 12%, transparent); color: var(--color-warning); } .alert-danger { background-color: color-mix(in srgb, var(--color-danger) 12%, transparent); color: var(--color-danger); }关于color-mix()函数需要说明一下它可以在 CSS 中直接混合两个颜色例如创建一个带透明度的背景色。这个函数属于较新的 CSS 颜色特性Chrome 111、Edge 111 以及新版 Safari、Firefox 都已支持。如果你的项目需要兼容旧浏览器可以退一步直接写rgba色值.tag-success { background-color: rgba(111, 191, 143, 0.12); }4.4 编写主题切换脚本文件路径js/theme.js主题切换需要满足三个要求点击按钮在亮色和暗色之间切换。刷新页面后依然保持用户选择。如果用户未选择则跟随系统偏好。// 主题管理逻辑 (function () { const STORAGE_KEY peach-theme; const html document.documentElement; // 获取当前主题 // 优先级本地存储 系统偏好 默认亮色 function getTheme() { const saved localStorage.getItem(STORAGE_KEY); if (saved light || saved dark) { return saved; } const prefersDark window.matchMedia((prefers-color-scheme: dark)).matches; return prefersDark ? dark : light; } // 应用主题 function applyTheme(theme) { html.setAttribute(data-theme, theme); // 同步按钮文案便于用户感知当前状态 const toggleBtn document.getElementById(theme-toggle); if (toggleBtn) { toggleBtn.textContent theme dark ? 切换到亮色 : 切换到暗色; } updateStatus(theme); } // 更新状态展示 function updateStatus(theme) { const statusEl document.getElementById(theme-status); if (statusEl) { statusEl.textContent theme dark ? 当前模式暗色 : 当前模式亮色; } } // 切换主题 function toggleTheme() { const current html.getAttribute(data-theme) dark ? dark : light; const next current dark ? light : dark; localStorage.setItem(STORAGE_KEY, next); applyTheme(next); } // 监听系统偏好变化仅当用户未手动选择时生效 window.matchMedia((prefers-color-scheme: dark)).addEventListener(change, function (e) { const saved localStorage.getItem(STORAGE_KEY); if (!saved) { applyTheme(e.matches ? dark : light); } }); // 初始化 function init() { applyTheme(getTheme()); const toggleBtn document.getElementById(theme-toggle); if (toggleBtn) { toggleBtn.addEventListener(click, toggleTheme); } } // 避免脚本在 DOM 未准备好时执行 if (document.readyState loading) { document.addEventListener(DOMContentLoaded, init); } else { init(); } })();这里有一个值得注意的细节当系统偏好变化时如果用户没有手动选择过主题应该跟随新的系统偏好。如果用户已经手动选择了亮色那系统从亮色切到暗色时不应该强制改掉用户的选择。4.5 编写页面入口文件路径index.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / title彩彩与千圣水蜜桃恋色物语主题系统/title meta namedescription content基于 CSS 自定义属性构建的水蜜桃色系前端主题系统示例 / link relstylesheet hrefcss/variables.css / link relstylesheet hrefcss/base.css / link relstylesheet hrefcss/components.css / !-- 防止主题闪烁的脚本放在 head 中 -- script (function () { var saved localStorage.getItem(peach-theme); var prefersDark window.matchMedia((prefers-color-scheme: dark)).matches; var theme saved light || saved dark ? saved : prefersDark ? dark : light; document.documentElement.setAttribute(data-theme, theme); })(); /script /head body div classcontainer header classpage-header h1彩彩与千圣水蜜桃恋色物语/h1 p classsubtitle一套基于 CSS 自定义属性的水蜜桃色系主题系统示例/p div stylemargin-top: 16px; display: flex; gap: 12px; align-items: center button idtheme-toggle classbtn btn-primary切换到暗色/button span idtheme-status stylecolor: var(--color-text-secondary)当前模式亮色/span /div /header section div classcard h2 classcard-title按钮 Button/h2 div styledisplay: flex; flex-wrap: wrap; gap: 12px button classbtn btn-primary主要按钮/button button classbtn btn-default默认按钮/button button classbtn btn-danger危险按钮/button button classbtn btn-primary disabled禁用按钮/button /div /div div classcard h2 classcard-title标签 Tag/h2 div styledisplay: flex; flex-wrap: wrap; gap: 8px span classtag tag-success成功/span span classtag tag-warning警告/span span classtag tag-danger危险/span span classtag默认标签/span /div /div div classcard h2 classcard-title表单 Form/h2 div classform-item label forusername昵称/label input classinput idusername typetext placeholder请输入你的昵称 / /div div classform-item label forbio个人简介/label input classinput idbio typetext placeholder介绍一下自己吧 / /div /div div classcard h2 classcard-title提示框 Alert/h2 div classalert alert-info这是一条普通提示信息/div div classalert alert-success操作成功水蜜桃主题已生效/div div classalert alert-warning警告请仔细核对配置内容/div div classalert alert-danger错误无法保存当前设置/div /div /section /div script srcjs/theme.js/script /body /html页面结构并不复杂关键在于把前面定义的组件类逐一使用起来。你打开页面后应该能看到一组粉色调的按钮、卡片、表单和提示框并且点击右上角的切换按钮整个页面会在亮色与暗色之间平滑过渡。4.6 运行与验证在项目根目录执行npm start或者直接用 VS Code 的 Live Server 插件打开index.html。运行后可以打开浏览器控制台输入getComputedStyle(document.documentElement).getPropertyValue(--color-primary-default)查看主题切换后的变量变化。我整理了一份简单的验证清单检查项预期结果页面默认背景亮色模式为浅粉白色#fff7f4点击切换按钮>
返回列表