HTML&CSS&JS:丝滑无卡顿的明暗主题切换

这个 HTML 页面实现了一个现代化、丝滑过渡的白天/夜间主题切换功能,完全基于 CSS 自定义属性(CSS 变量)、light-dark() 函数和少量 JavaScript 实现持久化。


大家复制代码时,可能会因格式转换出现错乱,导致样式失效。建议先少量复制代码进行测试,若未能解决问题,私信回复源码两字,我会发送完整的压缩包给你。

演示效果

HTML&CSS

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>主题切换</title>
    <style>
        @charset "UTF-8";

        :root {
            --brand-color-oklch: oklch(65% 0.15 250);
            --light-bg: oklch(98% 0.01 250);
            --dark-bg: oklch(25% 0.05 250);
            --bg-color: light-dark(var(--light-bg), var(--dark-bg));
            --light-text: oklch(20% 0.05 250);
            --dark-text: oklch(95% 0.01 250);
            --text-color: light-dark(var(--light-text), var(--dark-text));
            --box-bg-light: oklch(from var(--brand-color-oklch) 90% c h);
            --box-bg-dark: oklch(from var(--brand-color-oklch) 35% c h);
            --box-bg: light-dark(var(--box-bg-light), var(--box-bg-dark));
            --box-text-light: oklch(from var(--brand-color-oklch) 25% c h);
            --box-text-dark: oklch(from var(--brand-color-oklch) 95% c h);
            --box-text: light-dark(var(--box-text-light), var(--box-text-dark));
            --toggle-track-light: oklch(80% 0.08 250);
            --toggle-track-dark: oklch(50% 0.12 250);
            --toggle-track: light-dark(var(--toggle-track-light),
                    var(--toggle-track-dark));
            --toggle-thumb-light: oklch(99% 0 0);
            --toggle-thumb-dark: oklch(30% 0.05 250);
            --toggle-thumb: light-dark(var(--toggle-thumb-light),
                    var(--toggle-thumb-dark));
            color-scheme: light;
        }

        html.dark {
            color-scheme: dark;
        }

        *,
        *::before,
        *::after {
            box-sizing: border-box;
        }

        body {
            background-color: var(--bg-color);
            color: var(--text-color);
            font-family: "Georgia", serif;
            display: grid;
            place-content: center;
            min-height: 100vh;
            margin: 0;
            gap: 2rem;
            transition: background-color 0.4s ease, color 0.4s ease;
        }

        .themed-box {
            background-color: var(--box-bg);
            color: var(--box-text);
            padding: 2rem 3rem;
            border-radius: 12px;
            font-size: 1.5rem;
            text-align: center;
            transition: background-color 0.4s ease, color 0.4s ease;
        }

        .toggle-wrap {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 0.75rem;
            font-size: 0.85rem;
            letter-spacing: 0.05em;
            text-transform: uppercase;
            opacity: 0.75;
        }

        #theme-toggle {
            position: absolute;
            opacity: 0;
            width: 0;
            height: 0;
        }

        .toggle-label {
            position: relative;
            display: inline-block;
            width: 56px;
            height: 30px;
            cursor: pointer;
        }

        .toggle-label::before {
            content: "";
            position: absolute;
            inset: 0;
            background: var(--toggle-track);
            border-radius: 999px;
            transition: background 0.4s ease;
        }

        .toggle-label::after {
            content: "";
            position: absolute;
            top: 3px;
            left: 3px;
            width: 24px;
            height: 24px;
            border-radius: 50%;
            background: var(--toggle-thumb);
            box-shadow: 0 1px 4px oklch(0% 0 0/0.25);
            transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.4s ease;
        }

        #theme-toggle:checked+.toggle-label::after {
            transform: translateX(26px);
        }

        .toggle-label .icon {
            position: absolute;
            top: 50%;
            transform: translateY(-50%);
            font-size: 14px;
            line-height: 1;
            pointer-events: none;
            user-select: none;
            transition: opacity 0.3s ease;
        }

        .icon-sun {
            left: 6px;
        }

        .icon-moon {
            right: 6px;
        }

        .icon-moon {
            opacity: 0.35;
        }

        #theme-toggle:checked~.toggle-wrap .icon-sun {
            opacity: 0.35;
        }
    </style>
</head>

<body>
    <div class="themed-box">
        Hello 我是超级酷炫且丝滑的主题页面!
    </div>

    <div class="toggle-wrap">
        <span>白天</span>
        <input type="checkbox" id="theme-toggle">
        <label for="theme-toggle" class="toggle-label" aria-label="Toggle dark mode">
            <span class="icon icon-sun" aria-hidden="true">☀️</span>
            <span class="icon icon-moon" aria-hidden="true">🌙</span>
        </label>
        <span>夜间</span>
    </div>

    <script>
        const toggle = document.getElementById("theme-toggle");
        const html = document.documentElement;

        const saved = localStorage.getItem("theme");
        if (saved === "dark") {
            html.classList.add("dark");
            toggle.checked = true;
        }

        toggle.addEventListener("change", () => {
            if (toggle.checked) {
                html.classList.add("dark");
                localStorage.setItem("theme", "dark");
            } else {
                html.classList.remove("dark");
                localStorage.setItem("theme", "light");
            }
        });

    </script>
</body>

</html>

HTML

  • div themed-box:主题展示盒。背景 / 文字色随明暗主题切换,用于直观展示主题效果。
  • div toggle-wrap:切换控件容器。包含「白天 / 夜间」文字、切换复选框、滑块标签。
  • div themed-box:展示内容区域,其背景色和文字颜色会随主题自动变化。
  • input checkbox theme-toggle:隐藏的复选框,作为主题切换的"状态控制器"。通过 :checked 伪类触发样式变化。
  • label theme-toggle:可点击的自定义开关按钮,关联到隐藏的 checkbox,提升无障碍访问性。
  • span icon-sun / icon-moon:分别显示太阳(☀️)和月亮(🌙)图标,通过透明度变化指示当前模式。

CSS

1. 核心色彩变量(OKLCH 模型)

css 复制代码
:root {
  /* 品牌基准色(OKLCH:亮度 65%,色度 0.15,色相 250) */
  --brand-color-oklch: oklch(65% 0.15 250);

  /* 页面背景色:light-dark() 自动适配系统/手动主题 */
  --light-bg: oklch(98% 0.01 250); /* 浅色主题背景(高亮低彩) */
  --dark-bg: oklch(25% 0.05 250); /* 深色主题背景(低亮中彩) */
  --bg-color: light-dark(var(--light-bg), var(--dark-bg));

  /* 文字色、卡片背景/文字色、切换滑块色均采用相同逻辑 */
  --text-color: light-dark(var(--light-text), var(--dark-text));
  --box-bg: light-dark(var(--box-bg-light), var(--box-bg-dark));
  /* ... 其他变量 */

  color-scheme: light; /* 默认浅色配色方案 */
}

/* 暗主题时切换配色方案 */
html.dark {
  color-scheme: dark;
}

核心

  • OKLCH 色彩模型:相比 RGB/HEX,更接近人眼感知,亮度(L)、色度(C)、色相(H)分离,主题切换时色彩更协调;
  • light-dark () 函数:自动根据 color-scheme 或手动主题切换变量值,无需重复写两套样式;
  • 色彩继承:通过 oklch(from var(--brand-color-oklch) L C H) 基于品牌色派生主题色,保证视觉统一性。

2. 全局布局与过渡动效

css 复制代码
body {
  background-color: var(--bg-color);
  color: var(--text-color);
  font-family: "Georgia", serif;
  display: grid;
  place-content: center; /* 垂直水平居中 */
  min-height: 100vh;
  margin: 0;
  gap: 2rem;
  transition: background-color 0.4s ease, color 0.4s ease; /* 背景/文字色过渡 */
}

.themed-box {
  background-color: var(--box-bg);
  color: var(--box-text);
  padding: 2rem 3rem;
  border-radius: 12px;
  text-align: center;
  transition: background-color 0.4s ease, color 0.4s ease; /* 卡片色过渡 */
}

核心

  • 网格布局实现内容居中,适配全屏幕;
  • 所有主题相关颜色(背景、文字、卡片)都加 0.4s 过渡,保证切换时「丝滑无卡顿」;
  • 卡片 themed-box 作为主题效果的直观展示载体,颜色随主题同步变化。

3. 切换滑块(Toggle)样式

css 复制代码
/* 隐藏原生复选框 */
#theme-toggle {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

/* 滑块轨道(label 伪元素) */
.toggle-label::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--toggle-track); /* 轨道色随主题变 */
  border-radius: 999px; /* 胶囊形轨道 */
  transition: background 0.4s ease;
}

/* 滑块按钮(label 伪元素) */
.toggle-label::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 24px;
  height: 24px;
  border-radius: 50%; /* 圆形按钮 */
  background: var(--toggle-thumb); /* 按钮色随主题变 */
  box-shadow: 0 1px 4px oklch(0% 0 0/0.25);
  /* 自定义缓动曲线,切换更丝滑 */
  transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.4s ease;
}

/* 选中时滑块右移 */
#theme-toggle:checked+.toggle-label::after {
  transform: translateX(26px);
}

/* 太阳/月亮图标显隐 */
.icon-sun { left: 6px; }
.icon-moon { right: 6px; opacity: 0.35; }
#theme-toggle:checked~.toggle-wrap .icon-sun { opacity: 0.35; }
#theme-toggle:checked~.toggle-wrap .icon-moon { opacity: 1; } /* 暗主题时月亮图标高亮 */

核心

  • 用 label 伪元素模拟滑块轨道 / 按钮,隐藏原生复选框,保证自定义样式;
  • 滑块移动用「自定义缓动曲线」(cubic-bezier),比默认 ease 更有「弹性感」;
  • 太阳 / 月亮图标随复选框状态切换透明度,视觉反馈更直观。

JavaScript

CSS 通过 html.dark 类触发暗主题,JS 负责添加 / 移除该类,并同步 localStorage,实现「主题持久化」。

js 复制代码
// ===================== 核心功能:明暗主题切换 + 主题持久化 =====================
// 1. 获取核心DOM元素(操作入口)
// 获取页面中id为"theme-toggle"的复选框(主题切换开关,视觉上隐藏,通过label触发)
const toggle = document.getElementById("theme-toggle");
// 获取HTML根元素,用于通过类名控制全局主题样式(CSS中通过html.dark触发暗主题)
const html = document.documentElement;

// 2. 页面初始化:恢复用户上次保存的主题(持久化核心)
// 从浏览器本地存储(localStorage)读取键为"theme"的值,localStorage数据永久保存(除非手动清除)
const saved = localStorage.getItem("theme");
// 如果本地存储中保存的主题是"dark"(暗主题)
if (saved === "dark") {
    // 给HTML根元素添加"dark"类,触发CSS中定义的暗主题样式(如背景/文字色切换)
    html.classList.add("dark");
    // 将切换按钮(复选框)设置为选中状态,保证按钮视觉状态与主题一致(暗主题时滑块在右侧)
    toggle.checked = true;
}
// 补充:如果本地存储无值/值为"light",则不执行上述逻辑,默认显示浅色主题

// 3. 监听切换按钮状态变化,实现实时主题切换
// 给复选框添加"change"事件监听:当复选框选中状态改变时触发回调函数
// 触发场景:用户点击切换滑块(label标签),复选框状态从选中→未选中/未选中→选中
toggle.addEventListener("change", () => {
    // 判断复选框当前是否为选中状态(用户切换到暗主题)
    if (toggle.checked) {
        // 给HTML根元素添加"dark"类,切换到暗主题样式
        html.classList.add("dark");
        // 将当前主题(dark)保存到本地存储,实现"刷新/重启页面不丢失主题"
        localStorage.setItem("theme", "dark");
    } else {
        // 复选框未选中(用户切换回浅色主题):移除HTML根元素的"dark"类
        html.classList.remove("dark");
        // 将当前主题(light)保存到本地存储,记录用户选择
        localStorage.setItem("theme", "light");
    }
});

各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

相关推荐
UIUV4 小时前
node:child_process spawn 模块学习笔记
javascript·后端·node.js
烛阴5 小时前
Three.js 零基础入门:手把手打造交互式 3D 几何体展示系统
javascript·webgl·three.js
颜酱5 小时前
单调栈:从模板到实战
javascript·后端·算法
_AaronWong7 小时前
Electron 实现仿豆包划词取词功能:从 AI 生成到落地踩坑记
前端·javascript·vue.js
JohnYan7 小时前
工作笔记-CodeBuddy应用探索
javascript·ai编程·aiops
wuhen_n8 小时前
双端 Diff 算法详解
前端·javascript·vue.js
光影少年8 小时前
说说闭包的理解和应用场景?
前端·javascript·掘金·金石计划
爱勇宝8 小时前
别再混用了!import.meta.env 与 process.env 的本质差异一次讲透
前端·javascript·vue.js
路修远i8 小时前
基于SSE的AI对话流式结构
前端·javascript