普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月18日首页

前端样式工程化三剑客:CSS Modules、Scoped CSS 与 CSS-in-JS 深度实战

2026年1月18日 15:37

前言:为什么我们需要“工程化”样式?

在早期的前端开发中,CSS 是全局的。我们写一个 .button { color: red },它会立刻影响页面上所有的按钮。这在小型项目中或许可行,但在大型应用、多人协作或开源组件库开发中,这无异于“灾难”——样式冲突、优先级战争(Specificity Wars)层出不穷。

为了解决这个问题,现代前端框架提出了三种主流的解决方案:

  1. CSS Modules (React 生态主流方案)
  2. Scoped CSS (Vue 生态经典方案)
  3. CSS-in-JS (Stylus Components 为代表的动态方案)

本文将通过三个实战 Demo (css-demo, vue-css-demo, styled-component-demo),带你深入理解这三种方案的原理、差异及面试考点。


第一部分:CSS Modules - 基于文件的模块化

场景描述:
css-demo 项目中,我们不再直接使用全局的 CSS,而是利用 Webpack 等构建工具,将 CSS 文件编译成 JavaScript 对象。

1.1 核心原理

CSS Modules 并不是一门新的语言,而是一种编译时的解决方案。

  • 编译机制:构建工具(如 Webpack)会将 .module.css 文件编译成一个 JS 对象。
  • 局部作用域:默认情况下,CSS Modules 中的类名是局部的。构建工具会将类名(如 .button)编译成唯一的哈希值(如 _src-components-Button-module__button__23_a0)。
  • 导入方式:在组件中,我们通过 import styles from './Button.module.css' 导入这个对象,然后通过 styles.button 动态绑定类名。

1.2 代码实战解析

在我们的 Demo 中,定义了一个按钮组件:

// Button.jsx
import styles from './Button.module.css';

export default function Button() {
  return (
    <button className={styles.button}>My Button</button>
  );
}

对应的样式文件:

/* Button.module.css */
.button {
  background-color: blue;
  color: white;
  padding: 10px 20px;
}

发生了什么?

  1. 构建工具读取 Button.module.css
  2. .button 转换为类似 _button_hash123 的唯一类名。
  3. 生成一个对象:{ button: '_button_hash123' }
  4. JSX 渲染时,className 变成了唯一的哈希值,实现了样式隔离。

1.3 答疑解惑与面试宝典

Q1:CSS Modules 是如何解决样式冲突的?

  • 答: 核心在于哈希化(Hashing) 。它利用构建工具,在编译阶段将局部类名映射为全局唯一的哈希类名。由于哈希值的唯一性,不同组件即使定义了同名的 .button,最终生成的 CSS 类名也是不同的,从而从根本上杜绝了冲突。

Q2:CSS Modules 和普通的 CSS import 有什么区别?

  • 答:

    • 普通 CSSimport './style.css' 只是引入了样式,类名依然是全局的。
    • CSS Modulesimport styles from './style.module.css' 将样式变成了 JS 对象,你必须通过对象的属性来引用类名,从而强制实现了作用域隔离。

Q3:如何在 CSS Modules 中使用全局样式?

  • 答: 虽然不推荐,但有时确实需要。可以通过 :global 伪类来声明:

    :global(.global-class) {
      color: red;
    }
    

    这样 global-class 就不会被哈希化,保持全局生效。


第二部分:Vue Scoped CSS - 属性选择器的魔法

场景描述:
vue-css-demo 项目中,我们使用 Vue 单文件组件(SFC)的经典写法,通过 <style scoped> 实现样式隔离。

2.1 核心原理

Vue 的 scoped 属性实现原理与 CSS Modules 截然不同,它采用的是属性选择器方案。

  • 编译机制:Vue Loader 会为组件中的每个 DOM 元素生成一个唯一的属性(例如 data-v-f3f3eg9)。
  • 样式重写:同时,它会将 <style scoped> 中的选择器(如 .txt)重写为属性选择器(如 .txt[data-v-f3f3eg9])。
  • 作用域限制:由于只有当前组件的 DOM 元素拥有该属性,样式自然只能作用于当前组件。

2.2 代码实战解析

在 Vue 的 Demo 中,我们有两个层级:App.vueHelloWorld.vue

<!-- App.vue -->
<template>
  <div>
    <h1 class="txt">Hello world in App</h1>
    <HelloWorld />
  </div>
</template>

<style scoped>
.txt {
  color: red;
}
</style>
<!-- HelloWorld.vue -->
<template>
  <div>
    <h1 class="txt">你好,世界!!!</h1>
  </div>
</template>

<style scoped>
.txt {
  color: blue;
}
</style>

发生了什么?

  1. 编译后,App.vue 中的 <h1> 标签被加上了 data-v-abc123 属性。
  2. App.vue 的 CSS 变成了 .txt[data-v-abc123] { color: red }
  3. 编译后,HelloWorld.vue 中的 <h1> 标签被加上了 data-v-xyz456 属性。
  4. HelloWorld.vue 的 CSS 变成了 .txt[data-v-xyz456] { color: blue }
  5. 结果:父子组件的 .txt 类名互不干扰,各自生效。

2.3 答疑解惑与面试宝典

Q1:Vue Scoped 的性能怎么样?

  • 答: 性能通常很好,但也有局限。它只生成一次属性,且利用了浏览器原生的属性选择器能力。但是,如果组件层级很深,属性选择器的权重会增加。此外,它无法穿透子组件(即父组件的 scoped 样式无法直接修改子组件的样式),这是它的设计初衷,也是需要注意的点。

Q2:如何修改子组件的样式?(深度选择器)

  • 答: 当需要修改第三方组件库(如 Element Plus)的样式时,scoped 会失效。Vue 提供了深度选择器:

    • Vue 2:使用 >>>/deep/
    • Vue 3:使用 :deep()
    /* Vue 3 写法 */
    .parent-class :deep(.child-class) {
      color: red;
    }
    

Q3:scoped 会导致样式权重增加吗?

  • 答: 会。 因为它变成了属性选择器,例如 .txt 变成了 .txt[data-v-123],其权重高于普通的类选择器。如果在全局样式中写了 .txt { color: blue },而在 scoped 中写了 .txt { color: red },scoped 的样式会因为权重更高而覆盖全局样式。

第三部分:Stylus Components - CSS-in-JS 的动态艺术

场景描述:
styled-component-demo 项目中,我们将 CSS 直接写在 JavaScript 文件中,通过模板字符串创建组件。

3.1 核心原理

CSS-in-JS 是一种运行时的解决方案(虽然也支持 SSR 和编译时优化)。

  • 组件即样式:它将样式直接绑定到组件上。你不是在组件中引用类名,而是直接创建一个“带样式的组件”。
  • 动态性:样式可以像 JS 变量一样使用,支持传参(Props)。这使得主题切换、动态样式变得非常简单。
  • 唯一性:生成的类名也是唯一的(通常基于组件名和哈希),确保不污染全局。

3.2 代码实战解析

// App.jsx
import styled from 'styled-components';

// 创建一个带样式的 Button 组件
const Button = styled.button`
  background: ${props => props.primary ? 'blue' : 'white'};
  color: ${props => props.primary ? 'white' : 'blue'};
  padding: 8px 16px;
`;

function App() {
  return (
    <>
      <Button>默认按钮</Button>
      <Button primary>主要按钮</Button>
    </>
  );
}

发生了什么?

  1. 组件定义styled.button 是一个函数,它接收模板字符串作为参数,返回一个 React 组件。
  2. 动态插值:在模板字符串中,我们可以使用 JavaScript 逻辑(如三元表达式)来根据 props 动态生成 CSS。
  3. 渲染:当 <Button primary> 渲染时,库会根据逻辑生成对应的 CSS 规则(如 background: blue),注入到 <head> 中,并将生成的唯一类名应用到 DOM 上。

3.3 答疑解惑与面试宝典

Q1:CSS-in-JS 的优缺点是什么?

  • 答:

    • 优点:极致的动态能力(基于 Props 的样式)、天然的组件隔离、支持主题(Theme)、解决了全局污染问题。
    • 缺点:运行时性能开销(需要 JS 计算生成 CSS)、CSS 文件体积无法单独缓存(随 JS 打包)、调试时类名可读性差(全是哈希)、学习成本较高。

Q2:CSS-in-JS 和 CSS Modules 哪个更好?

  • 答: 没有绝对的好坏,取决于场景。

    • CSS Modules:适合对性能要求极高、样式逻辑简单的项目,或者团队习惯传统的 CSS 写法。
    • CSS-in-JS:适合组件库开发、需要高度动态样式(如主题切换)、或者团队追求极致的组件封装性。

Q3:面试官问“你怎么看待把 CSS 写在 JS 里?”

  • 答: 这是一个经典的“分离关注点”争论。

    • 传统观点认为 HTML/CSS/JS 应该分离。
    • 现代组件化观点认为,组件才是关注点。一个 Button 组件的逻辑、结构和样式是紧密耦合的,放在一起更利于维护和复用。CSS-in-JS 正是这种理念的体现。

第四部分:三剑客终极对比与选型建议

为了让你更直观地理解,我整理了以下对比表:

特性 CSS Modules Vue Scoped CSS-in-JS (Stylus Components)
作用域机制 哈希类名 (编译时) 属性选择器 (编译时) 哈希类名 (运行时/编译时)
动态性 弱 (需配合 classnames 库) 中 (需配合动态 class 绑定) (直接使用 JS 逻辑)
学习成本 低 (仍是 CSS) 低 (Vue 特性) 中 (需学习新 API)
调试难度 低 (类名清晰) 中 (类名哈希化)
适用场景 大型 React 应用 Vue 2/3 项目 组件库、高动态 UI

选型建议:

  1. 如果你在用 Vue:首选 scoped,简单高效。如果项目非常复杂,可以考虑 CSS Modules 或 CSS-in-JS。

  2. 如果你在用 React

    • 如果追求性能和工程化规范,选 CSS Modules
    • 如果追求极致的组件封装和动态主题,选 CSS-in-JS (如 Stylus Components 或 Emotion)。
    • 如果是新项目,也可以考虑 Tailwind CSS 等 Utility-First 方案。

结语:样式工程化的未来

从全局 CSS 到现在的模块化、组件化,前端样式的发展始终围绕着**“隔离”“复用”**这两个核心矛盾。

CSS Modules 和 Vue Scoped 通过编译时手段解决了隔离问题,而 CSS-in-JS 则通过运行时手段赋予了样式以逻辑能力。

无论你选择哪一种方案,理解其背后的原理(哈希化、属性选择器、动态注入)都是至关重要的。希望这篇博客能帮助你在 css-demovue-css-demostyled-component-demo 三个项目中游刃有余,并在面试中脱颖而出。

最后的思考题:

  • 如果让你设计一个组件库(如 Ant Design),你会选择哪种方案?为什么?(提示:考虑主题定制和样式隔离的平衡)

附录:常见面试题汇总

  1. Vue scoped 的原理是什么?

    • 答:通过属性选择器。给组件元素加唯一属性,给样式加属性选择器。
  2. React 中如何实现 CSS Modules?

    • 答:文件名加 .module.css,导入为对象,通过对象属性绑定 className。
  3. CSS-in-JS 的性能瓶颈在哪里?

    • 答:运行时计算样式、注入 CSSOM 的操作(虽然现代库做了很多优化,如缓存)。
  4. 如何解决 CSS Modules 中的长类名问题?

    • 答:通常不需要解决,构建工具会压缩。如果在 DevTools 中调试,可以配置 Webpack 的 localIdentName 来生成可读的开发类名。
  5. Shadow DOM 和上述方案有什么区别?

    • 答:Shadow DOM 是浏览器原生的样式隔离方案,隔离性最强(完全独立的 DOM 树),但兼容性和集成成本较高。上述方案都是基于现有 DOM 的模拟隔离。

用纯 CSS3 打造《星球大战》片头字幕动画|前端特效实战

2026年1月18日 14:08

🌌 用纯 CSS3 打造《星球大战》片头字幕动画|前端特效实战

无需 JavaScript,仅用 HTML + CSS3 关键帧动画,复刻电影级 3D 字幕效果

大家好!今天带大家用 纯 CSS3 实现一个经典又酷炫的前端动画—— 《星球大战》开场字幕。这个效果利用了 perspectivetransform: translateZ()@keyframes 等 CSS3 特性,完美模拟了文字从屏幕前方飞向远方的 3D 视觉效果。

整个项目零 JS、零依赖,是学习 CSS 动画和 3D 变换的绝佳案例!


🎬 效果预览(文字描述)

  • 黑色宇宙背景(bg.jpg
  • “STAR WARS” 标题从远处飞入,逐渐放大后消失
  • 副标题 “The Force Awakens” 同样飞入飞出
  • 主文案(如 “A long time ago...”)以倾斜角度从底部向上滚动,最终消失在远方
  • 所有文字带有金属光泽渐变,增强科幻感

🧱 一、HTML 结构:语义化 + 精简

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Star Wars Intro - Pure CSS3</title>
  <link rel="stylesheet" href="style.css" />
</head>
<body>
  <div class="starwars">
    <img src="./star.svg" alt="STAR" class="star" />
    <img src="./wars.svg" alt="WARS" class="wars" />
    <div class="byline">
      <span>A</span><span> </span>
      <span>L</span><span>O</span><span>N</span><span>G</span>
      <!-- ... 每个字符用 span 包裹 ... -->
    </div>
  </div>
</body>
</html>

✅ 设计思路:

  • 使用 <img> 加载 “STAR” 和 “WARS” 的 SVG 图标(更易控制缩放与动画)
  • 主文案每个字符用 <span> 包裹 → 便于逐字控制动画
  • 容器 .starwars 作为 3D 舞台

💡 为什么用 13 个 <span>
因为要对每一个文字单独做 3D 旋转和透明度动画,行内元素必须转为 inline-block 才支持 transform


🎨 二、CSS 核心:3D 舞台 + 关键帧动画

1. 初始化:重置样式 + 全屏背景

/* 引入 Meyer Reset */
html, body { margin: 0; padding: 0; }

body {
  height: 10 h;
  background: #000 url(./bg.jpg);
}

2. 创建 3D 舞台(关键!)

.starwars {
  perspective: 800px;           /* 模拟人眼到屏幕的距离 */
  transform-style: preserve-3d; /* 保持子元素 3D 变换 */
  width: 34em;
  height: 17em;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

🔍 perspective: 800px 是实现“纵深感”的核心!值越小,3D 效果越强。

3. 文字定位

.star, .wars, .byline {
  position: absolute;
}

.star { top: -0.75em; }
.wars { bottom: -0.5em; }
.byline {
  top: 45%;
  text-align: center;
  letter-spacing: 0.4em;
  font-size: 1.6em;
  /* 金属渐变文字 */
  background: linear-gradient(90deg, #fff 0%, #000 50%, #fff 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent; /* 让渐变生效 */
}

4. 行内元素支持 3D 旋转

.byline span {
  display: inline-block; /* 必须!否则 rotateY 无效 */
}

🌀 三、动画实现:@keyframes 关键帧

1. STAR/WARS 飞入飞出

.star {
  animation: star 10s ease-out infinite;
}

@keyframes star {
  0% {
    opacity: 0;
    transform: scale(1.5) translateY(-0.75em);
  }
  20% { opacity: 1; }
  90% { opacity: 1; transform: scale(1); }
  100% {
    opacity: 0;
    transform: translateZ(-1000em); /* 飞向远方 */
  }
}

translateZ(-1000em) 是让元素“远离屏幕”的关键!

2. 主文案滚动 + 逐字旋转

.byline {
  animation: move-byline 10s linear infinite;
}

.byline span {
  animation: spin-letters 10s linear infinite;
}

@keyframes move-byline {
  0% { transform: translateZ(5em); }   /* 从近处开始 */
  100% { transform: translateZ(0); }   /* 向远处移动 */
}

@keyframes spin-letters {
  0%, 10% {
    opacity: 0;
    transform: rotateY(90deg); /* 初始“侧躺” */
  }
  30% { opacity: 1; }
  70%, 86% {
    opacity: 1;
    transform: rotateY(0deg); /* 正面朝向 */
  }
  95%, 100% { opacity: 0; }
}

💡 rotateY(90deg) 让文字像“钢管舞”一样从侧面转正,增强动感!


🛠️ 四、技术亮点总结

技术点 作用
perspective 创建 3D 视觉空间
transform-style: preserve-3d 保留子元素 3D 变换
translateZ() 控制元素在 Z 轴(纵深)位置
@keyframes 定义复杂动画流程
display: inline-block 使行内元素支持 3D 变换
background-clip: text 实现文字渐变填充

📦 五、项目结构

star-wars-css/
├── index.html
├── style.css
├── readme.md
├── bg.jpg          # 宇宙背景图
├── star.svg        # "STAR" Logo
└── wars.svg        # "WARS" Logo

✅ 所有资源本地化,开箱即用!


🚀 六、如何运行?

  1. 下载全部文件
  2. 在浏览器中打开 index.html
  3. 享受你的星球大战时刻!

💬 结语

通过这个项目,我们不仅复刻了一个经典电影特效,更深入理解了:

  • CSS 3D 变换的核心原理
  • 如何用 translateZ 模拟纵深运动
  • 关键帧动画的精细控制
  • 行内元素的动画限制与突破

CSS 不只是样式,更是动画引擎!


🔗 所用图片

star.svg

bg.jpg

wars.svg

CSS Container Queries:实现响应式设计的新思路

2026年1月17日 19:10

CSS Container Queries:实现响应式设计的新思路

作为一名前端开发者,我相信你一定对媒体查询(Media Queries)不陌生。多年来,我们一直依赖 @media 规则来创建响应式设计,根据屏幕尺寸调整样式。但随着组件化开发的普及和设计复杂性的增加,我们逐渐发现了媒体查询的局限性。今天,我想和大家分享一个激动人心的新特性——CSS Container Queries,它正在改变我们思考和实现响应式设计的方式。

媒体查询的困境

在深入了解 Container Queries 之前,让我们先回顾一下传统媒体查询的限制。

想象这样一个场景:你正在开发一个卡片组件,这个组件可能会出现在页面的不同位置——有时占据整个宽度,有时只占据侧边栏的一小部分。使用传统的媒体查询,我们只能基于整个视口的尺寸来调整样式:

.card {
  padding: 1rem;
  background: white;
  border-radius: 8px;
}

.card h2 {
  font-size: 1.2rem;
}

@media (min-width: 768px) {
  .card h2 {
    font-size: 1.5rem;
  }
}

这种方法的问题在于,即使卡片本身很小(比如在侧边栏中),但如果视口宽度超过了768px,标题仍然会使用较大的字体,这可能导致布局问题。

Container Queries 的革命性思路

Container Queries 的出现解决了这个根本问题。它允许我们基于容器的尺寸而不是视口的尺寸来应用样式。这意味着组件可以根据自己的实际可用空间来调整外观,真正实现了组件级别的响应式设计。

基本语法和使用

要使用 Container Queries,首先需要定义一个容器:

.card-container {
  container-type: inline-size;
  /* 或者使用简写 */
  container: card-container / inline-size;
}

然后就可以使用 @container 规则了:

.card {
  padding: 1rem;
  background: white;
  border-radius: 8px;
}

.card h2 {
  font-size: 1.2rem;
}

@container (min-width: 400px) {
  .card h2 {
    font-size: 1.5rem;
  }
  
  .card {
    padding: 2rem;
  }
}

@container (min-width: 600px) {
  .card {
    display: flex;
    align-items: center;
  }
  
  .card h2 {
    font-size: 1.8rem;
  }
}

实际应用案例

让我通过一个完整的例子来展示 Container Queries 的强大之处。假设我们要创建一个产品卡片组件,它需要在不同的容器中表现出不同的布局:

<div class="main-content">
  <div class="product-card">
    <img src="product.jpg" alt="产品图片">
    <div class="product-info">
      <h3>产品标题</h3>
      <p>产品描述文本...</p>
      <div class="product-price">¥199</div>
      <button>立即购买</button>
    </div>
  </div>
</div>

<aside class="sidebar">
  <div class="product-card">
    <!-- 相同的HTML结构 -->
  </div>
</aside>

CSS实现:

/* 定义容器 */
.main-content,
.sidebar {
  container-type: inline-size;
}

/* 基础样式 */
.product-card {
  background: white;
  border-radius: 12px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  overflow: hidden;
}

.product-card img {
  width: 100%;
  height: 200px;
  object-fit: cover;
}

.product-info {
  padding: 1rem;
}

.product-info h3 {
  font-size: 1.1rem;
  margin-bottom: 0.5rem;
}

.product-price {
  font-size: 1.2rem;
  font-weight: bold;
  color: #e74c3c;
  margin: 0.5rem 0;
}

/* 中等尺寸容器 */
@container (min-width: 320px) {
  .product-card {
    display: flex;
  }
  
  .product-card img {
    width: 150px;
    height: 120px;
    flex-shrink: 0;
  }
  
  .product-info h3 {
    font-size: 1.2rem;
  }
}

/* 大尺寸容器 */
@container (min-width: 500px) {
  .product-card img {
    width: 200px;
    height: 150px;
  }
  
  .product-info {
    padding: 1.5rem;
  }
  
  .product-info h3 {
    font-size: 1.4rem;
  }
  
  .product-price {
    font-size: 1.4rem;
  }
}

容器类型详解

Container Queries 支持几种不同的容器类型:

1. inline-size

这是最常用的类型,监听容器的内联尺寸(通常是宽度):

.container {
  container-type: inline-size;
}

2. size

监听容器的所有尺寸(宽度和高度):

.container {
  container-type: size;
}

@container (min-width: 400px) and (min-height: 300px) {
  /* 样式规则 */
}

3. normal

默认值,不创建容器查询上下文。

命名容器查询

为了更好地组织代码,我们可以给容器命名:

.sidebar {
  container: sidebar-container / inline-size;
}

.main-content {
  container: main-container / inline-size;
}

@container sidebar-container (max-width: 300px) {
  .product-card {
    /* 侧边栏特定样式 */
  }
}

@container main-container (min-width: 800px) {
  .product-card {
    /* 主内容区特定样式 */
  }
}

与CSS Grid/Flexbox的完美结合

Container Queries 与现代布局技术结合使用时威力更大:

.grid-container {
  container-type: inline-size;
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}

.grid-item {
  container-type: inline-size;
}

@container (min-width: 300px) {
  .grid-item .content {
    display: flex;
    flex-direction: column;
    gap: 1rem;
  }
}

@container (min-width: 400px) {
  .grid-item .content {
    flex-direction: row;
    align-items: center;
  }
}

性能考量和最佳实践

在使用 Container Queries 时,有几个重要的性能和使用原则:

1. 避免循环依赖

确保容器的尺寸不依赖于其内容的查询结果:

/* 避免这样做 */
.container {
  container-type: inline-size;
  width: fit-content; /* 可能导致循环依赖 */
}

2. 合理使用容器类型

只有在真正需要时才设置 container-type: size,因为它的性能开销比 inline-size 更大。

3. 渐进增强

为不支持 Container Queries 的浏览器提供回退方案:

/* 回退样式 */
.card {
  padding: 1rem;
}

.card h2 {
  font-size: 1.2rem;
}

/* 支持 Container Queries 时的增强 */
@supports (container-type: inline-size) {
  .card-container {
    container-type: inline-size;
  }
  
  @container (min-width: 400px) {
    .card {
      padding: 2rem;
    }
    
    .card h2 {
      font-size: 1.5rem;
    }
  }
}

浏览器兼容性和Polyfill

截至2024年,Container Queries 已经在现代浏览器中得到了良好支持:

  • Chrome 105+
  • Firefox 110+
  • Safari 16+

对于需要支持旧版浏览器的项目,可以考虑使用 polyfill 或采用渐进增强的策略。

实际项目中的应用场景

1. 组件库开发

在开发可复用组件时,Container Queries 让组件真正做到了自适应:

.button-group {
  container-type: inline-size;
  display: flex;
  gap: 0.5rem;
}

@container (max-width: 200px) {
  .button-group {
    flex-direction: column;
  }
}

2. 复杂布局系统

在复杂的后台管理系统中,不同区域的组件可以根据实际空间灵活调整:

.dashboard-widget {
  container-type: inline-size;
}

@container (min-width: 300px) {
  .chart-widget {
    /* 显示完整图表 */
  }
}

@container (max-width: 299px) {
  .chart-widget {
    /* 显示简化版本 */
  }
}

CSS Container Queries 代表了响应式设计思维的重大转变。从关注全局视口到关注局部容器,这种变化让我们能够创建更加灵活、可复用的组件。虽然它还是一个相对较新的特性,但我相信随着浏览器支持的完善和开发者认知的提升,Container Queries 将成为现代前端开发的重要工具。

作为前端开发者,我建议大家开始在新项目中尝试使用 Container Queries,特别是在组件化开发中。它不仅能解决传统媒体查询的局限性,更能让我们的代码更加模块化和可维护。

响应式设计的未来已经到来,你准备好拥抱这个变化了吗?

昨天以前首页

电商都在用的 Sticky Sidebar,原来是这样实现的!

作者 阿明Drift
2026年1月16日 22:41

在电商、内容类网站中,“粘性侧边栏” 是非常常见的交互设计 —— 滚动页面时,侧边栏(如商品规格、筛选条件)始终保持可视,能显著提升用户体验。但实现过程中,我们常会遇到布局冲突、动态内容导致 sticky 失效等问题。本文将从基础原理到进阶适配,拆解一个 “智能粘性侧边栏” 的实现思路。

最近在浏览海外电商平台时,注意到一个高频出现的交互细节:产品详情页的侧边栏会“粘性固定”。无论左侧是图片轮播区,还是右侧是商品信息/购买按钮区,只要其中一侧内容较短,它就会在用户滚动页面时自动“吸顶”,始终保持在可视区域内。

9-sticky_effect1.gif

还有一些官网介绍页也有这种效果

9-sticky_effect2.gif

这种 Sticky Sidebar(粘性侧边栏) 效果极大提升了用户体验——用户无需反复滚动回顶部就能看到关键信息或操作按钮。

作为前端,必须学习借鉴一下。今天就一起深入理解下 position: sticky 的工作原理,并手写一个响应式 Sticky Sidebar 的 HTML Demo。

一、position: sticky 基础:粘住,但不 “越界”

position: sticky 是 CSS 中非常实用的定位属性,它兼具relativefixed的特性:

  • 当用户滚动页面、该元素尚未到达指定的粘附阈值(如 top: 20px)时,它表现为 relative 定位,随文档流正常布局;
  • 一旦滚动使其达到阈值(元素顶部距离视口顶部为 20px),它就会“粘住”在视口的指定位置(顶部20px处),表现得像 fixed 定位;
  • 但这种“固定”仅在其父容器的边界内有效——当父容器完全滚出视口后,该元素也会随之离开,不再固定。

总结一句话sticky 元素在滚动到阈值前表现如 relative,之后表现如 fixed,但始终被限制在父容器内

核心粘性样式定义如下:

.sticky-sidebar_sticky {
    position: sticky;
    top: 20px; /* 滚动到距离视口顶部20px时触发粘性 */
    z-index: 10;
}

⚠️ 注意:sticky 定位必须配合至少一个 toprightbottomleft 值才能生效。

二、Sticky 拟人化比喻:方形的女孩与视口顶端的男孩

光看定义太抽象。我自己强行想了个类比来加深记忆:

9-css-sticky-explainer-diagram.png

想象有一个 被拍扁成方形的女孩,她只能在家(父容器)里,从小被父母“金屋藏娇”——她永远不能离开这个房间(即不能脱离父元素的边界)。

在女孩家上空,视口顶部(top: 0)挂着一个 被拧成一条线的男孩,处在浏览器视口的上边缘。女孩头朝向男孩。

  1. 当页面刚开始向下滚动时(视口向下移动),男孩逐渐靠近女孩。

  2. 一旦女孩的头碰到男孩所在的位置(top: 0男孩立马“粘住”了她,带着她在房间内继续“移动”——此时女孩表现为 fixed 定位,粘在视口顶部。

  3. 男孩带着女孩继续在家里“移动”,但注意!她依然不能走出房间。如果男孩飘出女孩家(父容器滚动出视口),她也停留在房间内,男孩女孩暂时分离了。

  4. 当页面向上回滚时,男孩接触到女孩头部时,男孩又会“粘住”她,直到把她带回她最初的那个位置——也就是她在房间里的原始坐标。这时她又变回 relative 定位。

三、实战:手写一个 Sticky Sidebar Demo

我参考主流实现方式,写了一个简洁的 HTML 示例。(可以复制保存到本地看效果)以下是完整代码:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Sticky Sidebar with Bottom Alignment</title>
        <style>
            * {
                margin: 0;
                padding: 0;
                box-sizing: border-box;
            }

            body {
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                background: #f9f9f9;
                padding: 40px 20px;
                line-height: 1.6;
                color: #333;
            }

            .sticky-sidebar {
                display: block;
                width: 100%;
            }

            .sticky-sidebar__container {
                display: grid;
                grid-template-columns: 1fr 1fr;
                gap: 40px;
                align-items: flex-start; /* 👈 关键!避免子项被 stretch */
                max-width: 1200px;
                margin: 0 auto;
                padding: 40px 20px;
                background: white;
                border-radius: 16px;
                box-shadow: 0 6px 24px rgba(0, 0, 0, 0.08);
            }

            /* 加深 .sticky-sidebar__content 的阴影 */
            .sticky-sidebar__content {
                background-color: #ffffff;
                padding: 24px;
                border-radius: 12px;
                /* 增加阴影的垂直偏移、模糊半径、扩散半径和颜色,使边缘更明显 */
                box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
            }

            /* 👇 核心:sticky 行为 */
            .sticky-sidebar__sticky {
                position: sticky;
                top: 20px;
                z-index: 10;
            }

            .image-placeholder {
                width: 150px;
                height: 150px;
                background-color: #e0e0e0;
                color: #000000;
                border-radius: 6px;
                display: flex;
                justify-content: center;
                align-items: center;
                font-family: Arial, sans-serif;
                font-size: 14px;
                text-align: center;
                padding: 10px;
            }

            @media screen and (max-width: 989px) {
                .sticky-sidebar__container {
                    grid-template-columns: 1fr;
                    gap: 24px;
                }

                .sticky-sidebar__sticky {
                    position: static !important;
                }
            }

            /* 推荐商品区域 */
            .recommended-products {
                max-width: 1200px;
                margin: 80px auto 0;
                padding: 0 20px;
            }

            .recommended-products h2 {
                text-align: center;
                margin-bottom: 32px;
                font-size: 28px;
                color: #111;
            }

            .product-grid {
                display: grid;
                grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
                gap: 24px;
            }

            .product-card {
                background: white;
                border-radius: 12px;
                overflow: hidden;
                box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
                transition: transform 0.2s;
            }

            .product-card:hover {
                transform: translateY(-4px);
            }

            .product-card img {
                width: 100%;
                height: 200px;
                object-fit: cover;
                background: #eee;
            }

            .product-card .info {
                padding: 16px;
            }

            .product-card .info h3 {
                font-size: 18px;
                margin-bottom: 8px;
            }

            .product-card .info .price {
                color: #e53935;
                font-weight: bold;
            }

            /* 滚动提示 */
            .scroll-hint {
                text-align: center;
                margin-top: 40px;
                color: #888;
                font-style: italic;
            }
        </style>
    </head>

    <body>
        <!-- 主 Sticky 区域 -->
        <sticky-sidebar class="sticky-sidebar" data-sticky-offset="20">
            <div class="sticky-sidebar__container">
                <!-- 左侧:短内容 -->
                <div class="sticky-sidebar__left" data-sidebar-side="left">
                    <div class="sticky-sidebar__content">
                        <h2>🏃‍♂️ Product Media</h2>
                        <p>This is the product image/video gallery area.</p>
                        <div
                            style="
                background: #eee;
                height: 300px;
                margin-top: 16px;
                border-radius: 8px;
                display: flex;
                align-items: center;
                justify-content: center;
              "
                        >
                            [Product Image]
                        </div>
                        <p style="margin-top: 16px; font-size: 14px; color: #666">
                            (Short content — will stick while scrolling)
                        </p>

                        <!-- 动态内容:可展开的图片库 -->
                        <details
                            style="
                margin-top: 20px;
                padding: 12px;
                background: #f0f0f0;
                border-radius: 8px;
                cursor: pointer;
              "
                        >
                            <summary style="font-weight: bold; user-select: none">
                                🖼️ More Images (Click to expand)
                            </summary>
                            <div
                                style="
                  margin-top: 12px;
                  display: grid;
                  grid-template-columns: 1fr 1fr;
                  gap: 8px;
                "
                            >
                                <div class="image-placeholder">[image 1]</div>
                                <div class="image-placeholder">[image 2]</div>
                                <div class="image-placeholder">[image 3]</div>
                                <div class="image-placeholder">[image 4]</div>
                            </div>
                        </details>
                    </div>
                </div>

                <!-- 右侧:超长内容 -->
                <div class="sticky-sidebar__right" data-sidebar-side="right">
                    <div class="sticky-sidebar__content">
                        <h2>🛒 Variants & Add to Cart</h2>
                        <p>Select your size, color, and add to cart below.</p>

                        <div style="margin: 20px 0">
                            <label><strong>Size:</strong></label>
                            <select
                                style="
                  width: 100%;
                  padding: 10px;
                  margin-top: 6px;
                  border: 1px solid #ddd;
                  border-radius: 6px;
                "
                            >
                                <option>US 7</option>
                                <option>US 8</option>
                                <option>US 9</option>
                                <option>US 10</option>
                                <option>US 11</option>
                                <option>US 12</option>
                            </select>
                        </div>

                        <button
                            style="
                background: #1a73e8;
                color: white;
                border: none;
                padding: 14px 24px;
                font-size: 18px;
                border-radius: 8px;
                width: 100%;
                margin: 20px 0;
              "
                        >
                            Add to Cart
                        </button>

                        <hr style="margin: 30px 0; border: 0; border-top: 1px solid #eee" />

                        <h3>📝 Product Description</h3>
                        <p>
                            This premium running shoe features lightweight mesh, responsive foam,
                            and durable outsole.
                        </p>

                        <!-- 动态内容:可展开的详细介绍 -->
                        <details
                            style="
                margin-top: 30px;
                padding: 16px;
                background: #f9f9f9;
                border-radius: 8px;
                cursor: pointer;
              "
                        >
                            <summary style="font-weight: bold; font-size: 16px; user-select: none">
                                📖 Detailed Features & Benefits
                            </summary>
                            <div style="margin-top: 16px; line-height: 1.8; color: #555">
                                <h4 style="margin-top: 16px; margin-bottom: 8px; color: #333">
                                    Advanced Cushioning Technology
                                </h4>
                                <p>
                                    Our premium running shoes feature cutting-edge cushioning
                                    technology that provides exceptional comfort and support. The
                                    multi-layer foam construction absorbs impact while maintaining
                                    responsiveness, allowing you to run longer with less fatigue.
                                </p>

                                <h4 style="margin-top: 16px; margin-bottom: 8px; color: #333">
                                    Breathable Mesh Upper
                                </h4>
                                <p>
                                    The engineered mesh upper ensures maximum breathability, keeping
                                    your feet cool and dry during intense workouts. The strategic
                                    ventilation zones allow air to flow freely, preventing moisture
                                    buildup and odor formation even during extended running
                                    sessions.
                                </p>

                                <h4 style="margin-top: 16px; margin-bottom: 8px; color: #333">
                                    Durable Outsole
                                </h4>
                                <p>
                                    The reinforced rubber outsole is designed to withstand rigorous
                                    use on various terrains. With our proprietary grip pattern,
                                    you'll experience superior traction on both wet and dry
                                    surfaces, ensuring safety and confidence with every stride.
                                </p>

                                <h4 style="margin-top: 16px; margin-bottom: 8px; color: #333">
                                    Lightweight Design
                                </h4>
                                <p>
                                    Weighing just 7.2 ounces per shoe, our design minimizes energy
                                    expenditure while maintaining structural integrity. The
                                    lightweight construction allows for faster acceleration and
                                    smoother transitions, making it ideal for both casual joggers
                                    and competitive runners.
                                </p>

                                <h4 style="margin-top: 16px; margin-bottom: 8px; color: #333">
                                    Sustainability
                                </h4>
                                <p>
                                    We're committed to environmental responsibility. Our shoes are
                                    crafted using 30% recycled materials, reducing waste without
                                    compromising performance. The eco-friendly manufacturing process
                                    minimizes water usage and carbon emissions, making this a
                                    responsible choice for conscious consumers.
                                </p>

                                <h4 style="margin-top: 16px; margin-bottom: 8px; color: #333">
                                    Fit & Comfort
                                </h4>
                                <p>
                                    Designed with an ergonomic fit, these shoes conform to your
                                    foot's natural shape. The padded collar and tongue provide
                                    additional comfort, while the secure lacing system ensures a
                                    snug fit that reduces slippage and blisters during extended
                                    wear.
                                </p>

                                <h4 style="margin-top: 16px; margin-bottom: 8px; color: #333">
                                    Performance Metrics
                                </h4>
                                <p>Testing by professional athletes has shown:</p>
                                <ul style="margin-left: 20px; margin-top: 8px">
                                    <li>15% improvement in running efficiency</li>
                                    <li>25% reduction in impact-related fatigue</li>
                                    <li>40% increase in comfort rating vs. competitors</li>
                                    <li>99% durability over 300+ miles of running</li>
                                </ul>

                                <h4 style="margin-top: 16px; margin-bottom: 8px; color: #333">
                                    Care Instructions
                                </h4>
                                <p>
                                    To maintain optimal performance, hand wash with mild soap and
                                    cool water. Air dry naturally away from direct heat sources.
                                    Regular cleaning helps preserve the breathable mesh and extends
                                    the lifespan of your shoes.
                                </p>
                            </div>
                        </details>

                        <!-- 另一个可展开的动态内容 -->
                        <details
                            style="
                margin-top: 20px;
                padding: 16px;
                background: #f9f9f9;
                border-radius: 8px;
                cursor: pointer;
              "
                        >
                            <summary style="font-weight: bold; font-size: 16px; user-select: none">
                                ⭐ Customer Reviews
                            </summary>
                            <div style="margin-top: 16px">
                                <div
                                    style="
                    margin-bottom: 16px;
                    padding: 12px;
                    background: white;
                    border-left: 4px solid #ffc107;
                    border-radius: 4px;
                  "
                                >
                                    <p style="font-weight: bold; margin-bottom: 4px">
                                        John D. ⭐⭐⭐⭐⭐
                                    </p>
                                    <p>
                                        "Best shoes I've ever owned! The comfort is incredible, and
                                        they last forever. Highly recommend for anyone serious about
                                        running."
                                    </p>
                                </div>
                                <div
                                    style="
                    margin-bottom: 16px;
                    padding: 12px;
                    background: white;
                    border-left: 4px solid #ffc107;
                    border-radius: 4px;
                  "
                                >
                                    <p style="font-weight: bold; margin-bottom: 4px">
                                        Sarah M. ⭐⭐⭐⭐⭐
                                    </p>
                                    <p>
                                        "I've tried many brands, but these are my favorite. The
                                        support and cushioning are perfect. My feet feel amazing
                                        after long runs."
                                    </p>
                                </div>
                                <div
                                    style="
                    margin-bottom: 16px;
                    padding: 12px;
                    background: white;
                    border-left: 4px solid #ffc107;
                    border-radius: 4px;
                  "
                                >
                                    <p style="font-weight: bold; margin-bottom: 4px">
                                        Mike T. ⭐⭐⭐⭐
                                    </p>
                                    <p>
                                        "Great shoes! True to size, very comfortable. Only minor
                                        issue with sizing guide, but overall fantastic product."
                                    </p>
                                </div>
                                <div
                                    style="
                    margin-bottom: 16px;
                    padding: 12px;
                    background: white;
                    border-left: 4px solid #ffc107;
                    border-radius: 4px;
                  "
                                >
                                    <p style="font-weight: bold; margin-bottom: 4px">
                                        Emma L. ⭐⭐⭐⭐⭐
                                    </p>
                                    <p>
                                        "Perfect fit, amazing comfort level. These shoes transformed
                                        my running experience. Will definitely buy again!"
                                    </p>
                                </div>
                            </div>
                        </details>
                    </div>
                </div>
            </div>
        </sticky-sidebar>

        <!-- 👇 新增:推荐商品区域(让页面更长,并展示 sticky 自然结束) -->
        <div class="recommended-products">
            <h2>You May Also Like</h2>
            <div class="product-grid">
                <div class="product-card">
                    <img
                        src="https://via.placeholder.com/300x200/e0e0e0/000000?text=Trail+Shoe"
                        alt="Trail Shoe"
                    />
                    <div class="info">
                        <h3>Trail Running Shoe</h3>
                        <div class="price">$119.99</div>
                    </div>
                </div>
                <div class="product-card">
                    <img
                        src="https://via.placeholder.com/300x200/e0e0e0/000000?text=Gym+Shoe"
                        alt="Gym Shoe"
                    />
                    <div class="info">
                        <h3>Gym Training Shoe</h3>
                        <div class="price">$99.99</div>
                    </div>
                </div>
                <div class="product-card">
                    <img
                        src="https://via.placeholder.com/300x200/e0e0e0/000000?text=Running+Socks"
                        alt="Socks"
                    />
                    <div class="info">
                        <h3>Performance Socks (3-Pack)</h3>
                        <div class="price">$19.99</div>
                    </div>
                </div>
                <div class="product-card">
                    <img
                        src="https://via.placeholder.com/300x200/e0e0e0/000000?text=Water+Bottle"
                        alt="Bottle"
                    />
                    <div class="info">
                        <h3>Insulated Water Bottle</h3>
                        <div class="price">$29.99</div>
                    </div>
                </div>
                <div class="product-card">
                    <img
                        src="https://via.placeholder.com/300x200/e0e0e0/000000?text=Trail+Shoe"
                        alt="Trail Shoe"
                    />
                    <div class="info">
                        <h3>Trail Running Shoe</h3>
                        <div class="price">$119.99</div>
                    </div>
                </div>
                <div class="product-card">
                    <img
                        src="https://via.placeholder.com/300x200/e0e0e0/000000?text=Gym+Shoe"
                        alt="Gym Shoe"
                    />
                    <div class="info">
                        <h3>Gym Training Shoe</h3>
                        <div class="price">$99.99</div>
                    </div>
                </div>
                <div class="product-card">
                    <img
                        src="https://via.placeholder.com/300x200/e0e0e0/000000?text=Running+Socks"
                        alt="Socks"
                    />
                    <div class="info">
                        <h3>Performance Socks (3-Pack)</h3>
                        <div class="price">$19.99</div>
                    </div>
                </div>
                <div class="product-card">
                    <img
                        src="https://via.placeholder.com/300x200/e0e0e0/000000?text=Water+Bottle"
                        alt="Bottle"
                    />
                    <div class="info">
                        <h3>Insulated Water Bottle</h3>
                        <div class="price">$29.99</div>
                    </div>
                </div>
            </div>
        </div>

        <div class="scroll-hint">
            ✅ 尝试以下操作来观察 ResizeObserver 的实时效果:<br />
            1️⃣ 展开左侧 "More Images" → 左侧变高<br />
            2️⃣ 展开右侧 "Detailed Features & Benefits" → 右侧变高<br />
            3️⃣ 观察 sticky 策略是否动态调整(哪一侧保持固定)<br />
            4️⃣ 滚动到底部,观察 sticky 在容器结束时自然结束(不会穿透父容器)
        </div>

        <script>
            (function () {
                class StickySidebar extends HTMLElement {
                    constructor() {
                        super();
                        this.leftSide = null;
                        this.rightSide = null;
                        this.stickyOffset = 20;
                        this.resizeObserver = null;
                        this.isDesktop = window.innerWidth > 989;
                    }

                    connectedCallback() {
                        this.leftSide = this.querySelector('[data-sidebar-side="left"]');
                        this.rightSide = this.querySelector('[data-sidebar-side="right"]');
                        this.stickyOffset = parseInt(this.dataset.stickyOffset) || 20;

                        this.setupStickyBehavior();
                        this.setupResizeObserver();

                        window.addEventListener('resize', () => {
                            const wasDesktop = this.isDesktop;
                            this.isDesktop = window.innerWidth > 989;
                            if (wasDesktop !== this.isDesktop) {
                                this.setupStickyBehavior();
                            }
                        });
                    }

                    setupResizeObserver() {
                        if (!window.ResizeObserver) return;

                        this.resizeObserver = new ResizeObserver(() => {
                            if (this.isDesktop) {
                                setTimeout(() => this.setupStickyBehavior(), 50);
                            }
                        });

                        this.resizeObserver.observe(this.leftSide);
                        this.resizeObserver.observe(this.rightSide);
                    }

                    setupStickyBehavior() {
                        if (!this.isDesktop) {
                            this.leftSide.classList.remove('sticky-sidebar__sticky');
                            this.rightSide.classList.remove('sticky-sidebar__sticky');
                            return;
                        }

                        const leftHeight = this.leftSide.offsetHeight;
                        const rightHeight = this.rightSide.offsetHeight;

                        this.leftSide.classList.remove('sticky-sidebar__sticky');
                        this.rightSide.classList.remove('sticky-sidebar__sticky');

                        if (leftHeight < rightHeight) {
                            this.leftSide.classList.add('sticky-sidebar__sticky');
                            this.leftSide.style.top = this.stickyOffset + 'px';
                        } else if (rightHeight < leftHeight) {
                            this.rightSide.classList.add('sticky-sidebar__sticky');
                            this.rightSide.style.top = this.stickyOffset + 'px';
                        }
                    }
                }

                customElements.define('sticky-sidebar', StickySidebar);
            })();
        </script>
    </body>
</html>

1. 两列布局:Grid 实现 + flex/grid 布局的关键坑点

示例中采用 CSS Grid 实现两列布局,核心容器样式:

.sticky-sidebar_container {
    display: grid;
    grid-template-columns: 1fr 1fr; /*两列等分 */
    gap: 40px;
    align-items: flex-start; /* 重中之重 */
    max-width: 1200px;
    margin: 0 auto;
}

确保主内容区和侧边栏水平并排,且有合理间距。

为什么必须加 align-items: flex-start?

如果省略align-items: flex-start,会发生两个问题:

  1. 子元素被强制拉伸,即使内容本身很短,也会和另一列(长内容列)等高;
  2. sticky元素的 “父容器高度” 被撑满,粘性效果失去意义(元素本身已经占满父容器,已经没有在父容器的滚动空间了,滚动时不会触发 fixed)。

补充:如果用 Flex 实现两列布局,同样需要注意

/*Flex布局示例 */
.flex-container {
    display: flex;
    gap: 40px;
    align-items: flex-start; /* 必须加,否则sticky失效 */
}
.flex-container .col {
    flex: 1;
}

2. 进阶:ResizeObserver 监测动态高度,让sticky“智能切换”

示例中侧边栏包含可展开的details组件(如“更多图片”“详细特性”),展开/收起时列的高度会动态变化。如果仅靠初始高度判断哪一列加sticky,交互体验会割裂——因此需要ResizeObserver监测高度变化,动态调整粘性元素。

ResizeObserver 是什么?

ResizeObserver是浏览器原生API,用于监测元素的尺寸(宽/高)变化,触发回调函数。相比传统的window.resize(仅监测窗口变化),它能精准感知元素自身的尺寸变化,是处理动态内容的利器。

示例中的实现逻辑

  1. 初始化监测:连接DOM后,监听左右两列的尺寸变化
setupResizeObserver() {
  if (!window.ResizeObserver) return;

  this.resizeObserver = new ResizeObserver(() => {
    if (this.isDesktop) {
      // 延迟执行,确保DOM尺寸已更新
      setTimeout(() => this.setupStickyBehavior(), 50);
    }
  });

  // 监听左右两列的尺寸变化
  this.resizeObserver.observe(this.leftSide);
  this.resizeObserver.observe(this.rightSide);
}
  1. 动态调整粘性规则:对比两列高度,仅给“较短的列”添加sticky类
setupStickyBehavior() {
  if (!this.isDesktop) {
    // 移动端取消sticky,回归静态布局
    this.leftSide.classList.remove("sticky-sidebar__sticky");
    this.rightSide.classList.remove("sticky-sidebar__sticky");
    return;
  }

  // 获取当前两列的实际高度
  const leftHeight = this.leftSide.offsetHeight;
  const rightHeight = this.rightSide.offsetHeight;

  // 先清空所有sticky类
  this.leftSide.classList.remove("sticky-sidebar__sticky");
  this.rightSide.classList.remove("sticky-sidebar__sticky");

  // 智能判断:短的一列添加sticky
  if (leftHeight < rightHeight) {
    this.leftSide.classList.add("sticky-sidebar__sticky");
    this.leftSide.style.top = this.stickyOffset + "px";
  } else if (rightHeight < leftHeight) {
    this.rightSide.classList.add("sticky-sidebar__sticky");
    this.rightSide.style.top = this.stickyOffset + "px";
  }
}
  1. 响应式兼容:窗口尺寸变化时,同步更新布局逻辑
window.addEventListener('resize', () => {
    const wasDesktop = this.isDesktop;
    this.isDesktop = window.innerWidth > 989;
    // 仅当从桌面端/移动端切换时,重新设置sticky
    if (wasDesktop !== this.isDesktop) {
        this.setupStickyBehavior();
    }
});

3. 细节优化,完善交互体验

  1. 移动端降级:屏幕宽度<989px时,强制取消sticky(position: static !important),避免小屏上的布局错乱;
  2. z-index 层级:给sticky元素加z-index: 10,防止被其他内容遮挡;

4. <sticky-sidebar> Web Component 元素

<sticky-sidebar>是一个基于 Web Component 技术实现的自定义元素。Web Component 是浏览器原生支持的标准组件技术,相比 React、Vue 等框架组件具有跨框架兼容的优势,可以在不同技术栈之间直接复用。

其中,connectedCallback 方法相当于 React 中的 useEffect 钩子(组件挂载时执行)。

你可以根据具体的业务需求进一步扩展功能(例如自定义 sticky 触发阈值、适配多列布局等)。

总结

实现一个“健壮的粘性侧边栏”,需要兼顾三层:

  1. 基础层:掌握position: sticky的特性和边界;
  2. 布局层:Grid/Flex布局中,务必设置align-items: flex-start,避免子元素拉伸导致sticky失效;
  3. 动态层:用ResizeObserver监测元素高度变化,让sticky策略随内容动态调整。

这套思路不仅适用于电商商品页,也可迁移到博客侧边栏、后台管理系统等场景。核心是理解“sticky的生效条件”和“布局对定位的影响”,再结合原生API解决动态内容的适配问题。

position: sticky 是一个优雅而强大的 CSS 特性,它用极简的代码解决了复杂的滚动交互问题。理解其“相对+固定+受限”的三重特性,是用好它的关键。

注意事项 & 性能建议

  1. 父容器需有滚动上下文
    position: sticky 是否可见,取决于父容器是否有足够内容使其在滚动中“经过”视口。如果整个页面不可滚动,或父容器内无其他内容,sticky 行为将无法被触发——并非失效,而是缺乏滚动场景。

  2. 警惕 overflow 限制 sticky 范围
    避免在 sticky 元素与 body 之间意外插入 overflow: hidden/scroll/auto 的祖先元素,否则 sticky 的粘附范围会被限制在该容器内,可能不符合预期。

  3. 移动端兼容性良好
    现代浏览器(包括 iOS Safari 和 Android Chrome)均完整支持 position: sticky,可安全用于生产环境。

  4. 避免过度使用
    虽然 sticky 性能开销较小,但大量或嵌套使用可能引发布局抖动,尤其在低端设备上。保持简洁,只在必要处使用。


📚 参考资料

学习优秀作品,是提升技术的最佳路径。本文作为自己的学习笔记,也希望这篇解析对你有所帮助

Easy (Horizontal Scrollbar) Fixes for Your Blog CSS 博客 CSS 的简易(水平滚动条)修复

作者 i小溪
2026年1月16日 15:44

下面是对文章 Easy (Horizontal Scrollbar) Fixes for Your Blog CSS(来自 aartaka.me)的精炼总结


📌 文章核心:解决博客在小屏幕上出现 横向滚动条 的 CSS 问题

作者指出,在博客或网站上,一些常见元素往往会超出视口宽度,从而触发让人不舒服的横向滚动条。为此,他给出了简单有效的 CSS 解决方案应对最常见的三类情况:


🛠️ 实用修复方法

  1. 代码块 (<pre>) 内容太宽导致滚动条 ➤ 解决方案:让代码块自身可横向滚动,而不是整页滚动。

    pre {
        overflow-x: auto;
    }
    

    这样只有代码块在必要时滚动,不会破坏页面整体布局。

  2. 图片太大,超出容器宽度 ➤ 修复办法:限制图片最大宽度为容器宽度。

    img {
        max-width: 100%;
        height: auto;
    }
    

    这会让大图缩放以适应小视口。

  3. 表格宽度过大导致横向溢出 ➤ 解决思路:将表格包装在一个允许横向滚动的容器中:

    <div class="scrollable">
      <table></table>
    </div>
    
    .scrollable {
        overflow-x: auto;
    }
    

    让表格自己滚动,而非整个页面。


🧠 额外补充:处理长单词断行

对于 极长无分隔的单词(例如德语复合词),浏览器默认可能不换行造成溢出。作者建议:

  • 在合适位置插入 <wbr> 标签,允许浏览器断行;

  • 或者用 CSS 强制换行:

    p {
        overflow-wrap: anywhere;
    }
    

    (但作者不太推荐 CSS 方案)


✨ 总结

这篇文章提供了几条不用 JavaScript、纯用 CSS即可显著改善博客在窄屏设备上的展示体验的技巧,分别针对:

  • 代码块过宽
  • 图片尺寸失控
  • 表格宽度问题
  • (加分项)超长单词换行

这些都是现代博客常见导致横向滚动条的设计问题,修好它们能让移动端和小屏设备用户体验更佳。

Sass 模块化革命:告别 @import,拥抱 @use 和 @forward

作者 donecoding
2026年1月16日 14:01

为什么你的 Sass 代码突然开始报错?是时候彻底理解 Sass 的模块化系统了!

最近很多前端开发者突然发现自己的 Sass 代码开始报出各种警告和错误:

  • @import rules are deprecated
  • There's already a module with namespace "math"
  • Using / for division is deprecated

这一切都源于 Dart Sass 的模块化革命。如果你还在使用传统的 @import,那么这篇文章将带你彻底理解新的模块系统,并手把手教你如何迁移。

为什么要弃用 @import?

传统 @import 的问题

让我们先回顾一下 @import 的常见用法:

// variables.scss
$primary-color: #1890ff;
$font-size: 14px;

// main.scss
@import "variables";
@import "mixins";
@import "components/button";

.button {
  color: $primary-color;
  font-size: $font-size;
}

看起来很简单对吧?但 @import 有几个致命缺陷:

  1. 全局污染:所有变量、mixin、函数都混入全局作用域
  2. 无法避免冲突:同名变量会被覆盖,且很难追踪来源
  3. 无法控制可见性:无法隐藏私有变量
  4. 性能问题:每次 @import 都会重新计算
  5. 依赖混乱:无法知道模块间的依赖关系

新系统的优势

@use@forward 组成的模块系统解决了所有这些问题:

  • 命名空间隔离:每个模块有自己的作用域
  • 明确的依赖关系:清晰知道每个变量来自哪里
  • 更好的封装:可以隐藏私有成员
  • 更快的编译:模块只被计算一次

核心概念:@use vs @forward

@use:使用模块

@use 用于在当前文件中使用其他模块的功能。

// 基本用法
@use "sass:math";
@use "variables";

// 通过命名空间访问
.element {
  width: math.div(100%, 3);
  color: variables.$primary-color;
}

// 使用通配符(类似旧版行为)
@use "variables" as *;
.element {
  color: $primary-color; // 直接使用,无需前缀
}

// 自定义命名空间
@use "variables" as vars;
.element {
  color: vars.$primary-color;
}

@forward:转发模块

@forward 用于转发模块的成员,但不直接使用它们。常见于库的入口文件。

// 转发整个模块
@forward "variables";

// 选择性转发
@forward "sass:math" show div, ceil, floor;
@forward "components/button" hide _private-mixin;

// 重命名转发
@forward "sass:math" as math-*;
// 使用时会变成:math-div(), math-ceil()

实战迁移指南

场景1:基础变量和工具迁移

迁移前(@import):

// styles/variables.scss
$primary-color: #1890ff;
$border-radius: 4px;

// styles/mixins.scss
@mixin rounded-corners($radius: $border-radius) {
  border-radius: $radius;
}

// main.scss
@import "styles/variables";
@import "styles/mixins";

.button {
  color: $primary-color;
  @include rounded-corners;
}

迁移方案A:直接使用

// main.scss
@use "styles/variables" as vars;
@use "styles/mixins";

.button {
  color: vars.$primary-color;
  @include mixins.rounded-corners;
}

迁移方案B:创建库入口

// styles/_index.scss (库入口)
@forward "variables";
@forward "mixins";

// main.scss
@use "styles" as *; // 所有成员直接可用

.button {
  color: $primary-color;
  @include rounded-corners;
}

场景2:处理第三方库冲突

问题场景: 第三方库和你的代码都需要 sass:math

// ❌ 可能冲突的情况
// element-plus 内部已使用: @use "sass:math" as math;
// 你的代码中也使用: @use "sass:math" as math;

// ✅ 解决方案1:使用不同命名空间
@use "sass:math" as original-math;

.element {
  width: original-math.div(100%, 3);
}

// ✅ 解决方案2:创建包装函数
// utils/_math-utils.scss
@use "sass:math" as sass-math;

@function divide($a, $b) {
  @return sass-math.div($a, $b);
}

// 使用
@use "utils/math-utils" as math;
.element {
  width: math.divide(100%, 3);
}

场景3:构建组件库

项目结构:

ui-library/
├── foundation/
│   ├── _variables.scss
│   ├── _colors.scss
│   └── _index.scss
├── components/
│   ├── _button.scss
│   ├── _card.scss
│   └── _index.scss
└── index.scss

配置入口文件:

// ui-library/foundation/_index.scss
@forward "variables";
@forward "colors";
@forward "typography";

// ui-library/components/_index.scss
@forward "button" show button, button-variants;
@forward "card" show card;
// 隐藏私有成员
@forward "modal" hide _private-styles;

// ui-library/index.scss
@forward "foundation";
@forward "components";

// 业务代码中使用
@use "ui-library" as ui;

.custom-button {
  @extend ui.button;
  background-color: ui.$primary-color;
}

常见陷阱和解决方案

陷阱1:命名空间冲突

// ❌ 错误:相同的命名空间
@use "module1" as utils;
@use "module2" as utils; // 错误:命名空间 "utils" 重复

// ✅ 正确:使用不同的命名空间
@use "module1" as utils1;
@use "module2" as utils2;

陷阱2:@use 和 @forward 顺序错误

// ❌ 错误:@forward 必须在 @use 之前
@use "sass:color";
@forward "sass:math"; // 错误!

// ✅ 正确:正确的顺序
@forward "sass:math"; // 先转发
@use "sass:color";    // 后使用

陷阱3:忽略除法运算迁移

// ⚠️ 警告:传统除法将弃用
$ratio: 16/9; // 警告:Using / for division is deprecated

// ✅ 正确:使用 math.div()
@use "sass:math";
$ratio: math.div(16, 9);

陷阱4:在 @forward 文件中直接使用转发的成员

// utils/_index.scss
@forward "math-tools";

// ❌ 错误:不能在转发文件中直接使用转发的成员
$value: math.div(100, 2); // 错误!math 不可用

// ✅ 正确:需要单独 @use
@use "sass:math" as math;
$value: math.div(100, 2);
@forward "math-tools";

自动化迁移工具

Sass 官方提供了强大的迁移工具:

# 安装迁移工具
npm install -g sass-migrator

# 1. 迁移 @import 到 @use
sass-migrator import-to-use **/*.scss

# 2. 迁移除法运算
sass-migrator division **/*.scss

# 3. 同时处理多种文件类型
sass-migrator import-to-use --recursive "**/*.{scss,sass,vue}"

# 4. 带参数的迁移
sass-migrator import-to-use --namespace=lib "src/**/*.scss"

最佳实践总结

1. 命名策略

// 基础变量 → 通配符导入(方便使用)
@use "variables" as *;

// 工具函数 → 命名空间导入(避免冲突)
@use "utils/math" as math;

// 第三方库 → 使用短命名空间
@use "element-plus" as ep;

2. 文件组织

// 库/框架:使用 @forward 构建清晰的API
// _index.scss
@forward "foundation" show $colors, $typography;
@forward "components" hide _private-*;
@forward "utilities" as utils-*;

// 业务代码:使用 @use 明确依赖
@use "ui-library" as ui;
@use "project/utils" as utils;

3. 处理依赖关系

// 依赖图:A → B → C
// c.scss
$value: red;

// b.scss
@use "c" as *;
$color: $value;

// a.scss
@use "b" as *;
.element { color: $color; }

性能优化建议

  1. 减少重复计算:模块只计算一次,即使被多次 @use
  2. 合理使用缓存:构建工具通常会缓存编译结果
  3. 避免深层嵌套:过深的 @forward 链可能影响性能
  4. 按需导入:使用 show/hide 只导入需要的成员

版本兼容性

// package.json 版本建议
{
  "devDependencies": {
    "sass": "^1.58.0",     // 支持完整模块系统
    "sass-loader": "^13.2.0"
  }
}

写在最后

迁移到新的 Sass 模块系统看起来有些挑战,但带来的好处是实实在在的:

🎯 代码更清晰:明确的依赖关系和命名空间
🔧 维护更容易:模块化的结构便于重构
性能更好:智能的缓存和编译优化
🚀 面向未来:符合现代前端开发的最佳实践

迁移不是一次性的痛苦,而是一次性的投资。现在花时间迁移,未来将节省大量的调试和维护时间。

记住这个简单的决策流程:

  1. 构建库/框架 → 优先使用 @forward
  2. 编写业务代码 → 主要使用 @use
  3. 基础变量/配置 → 考虑 @use ... as *
  4. 工具函数 → 使用命名空间避免冲突

行动起来吧! 从今天开始,逐步将你的项目迁移到新的模块系统。你的未来代码库会感谢你现在做出的努力!


你的项目开始迁移了吗? 在迁移过程中遇到了什么有趣的问题或挑战?欢迎在评论区分享你的经验!

❌
❌