普通视图

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

2025 年 CSS 年度调查报告亮点速览

作者 冴羽
2026年1月20日 11:04

近日,「State of CSS 2025」年度调查报告公布。

这份报告收集了全球数万名开发者的真实使用经验和反馈,堪称是 Web 开发领域的“年度风向标”。

本篇我们盘点下这份报告的亮点部分。

1. 使用率最高的功能是 :has()

在调查的所有功能中,**:has()**是使用率最高也是最受欢迎的功能。

想必大家已经很熟悉了,它是一个功能非常强大的伪类,可以实现类似“父选择器”和“前面兄弟选择器”的功能。

举个简单的例子,下面的 CSS 代码表示如果 <a> 元素里面有 <img> 元素,则这个 <a> 元素就会匹配。

:has(img) {
  display: block;
}

我们可以使用这个选择器轻松区分是文字链接还是图像链接,并设置不同的 CSS 样式。

2. 使用率第二高的功能是 aspect-ratio

这个 CSS 属性允许你定义元素盒子的宽高比。

这意味着即使父容器或视口大小发生变化,浏览器也会调整元素的尺寸以保持指定的宽高比。

比如我们将一张图片设置为 3/2 宽高比:

img {
  aspect-ratio: 3/2;
}

3. 使用率最低的是 sibling-count 和 sibling-index

记得以前实现列表项交错动画时,要手动给每个元素设置不同的延迟吗?

现在,用 sibling-index() 一行代码就能搞定!

li {
  transition: opacity 0.3s ease;
  transition-delay: calc((sibling-index() - 1) * 100ms);
}

这个函数会自动获取元素在兄弟节点中的位置(从 1 开始计数),通过简单的计算就能实现流畅的交错动画效果

如果再搭配 @starting-style,连入场动画都能轻松搞定:

li {
  transition: opacity 0.3s ease;
  transition-delay: calc((sibling-index() - 1) * 100ms);

  @starting-style {
    opacity: 0;
  }
}

实现效果如下:

之所以使用率最低,可以理解,因为浏览器支持还比较新。

4. 受欢迎程度第二高的功能是 Subgrid

Subgrid 表示子网格,它并不是一个 CSS 属性,而是 grid-template-columns 和 grid-template-rows 属性支持的关键字,其使用的场景需要外面已经有个 Grid 布局。

什么时候会用到 Subgrid 呢?

举个例子,这是一个布局效果:

你会发现,标题字数不一样,内容字数不一样,导致底部很难对齐。

然而我们想要的效果是这样的:

此时就可以用到 Subgrid,使用示例如下:

.wrapper {
  display: grid;
  grid-template-columns: 1fr 1fr;
}

.item {
  grid-row: 1 / 4;
  display: grid;
  grid-template-rows: subgrid;
}

5. 认知度增长最高的是 light-dark()

不知道你是否实现过网站的浅色和深色主题:

:root {
  /* 默认浅色主题 */
  --text-heading: #000;
  --text-body: #212121;
  --surface: #efefef;

  @media (prefers-color-scheme: dark) {
    /* 暗色主题 - 第一遍 */
    --text-heading: #fff;
    --text-body: #efefef;
    --surface: #212121;
  }
}

.dark-theme {
  /* 暗色主题 - 又写一遍! */
  --text-heading: #fff;
  --text-body: #efefef;
  --surface: #212121;
}

同样的颜色写两遍,一个给媒体查询(自动切换),一个给切换按钮。

改一次要改两个地方,烦死了!

现在使用 light-dark() 轻松实现!

:root {
  /* 跟随系统偏好 */
  color-scheme: light dark;

  /* 一次定义,自动切换 */
  --text-heading: light-dark(#000, #fff);
  --text-body: light-dark(#212121, #efefef);
  --surface: light-dark(#efefef, #212121);
}

就这么简单!系统是浅色就用第一个,暗色就用第二个。

6. 评论最多的功能是 line-clamp,多是负面评价

CSS 属性 line-clamp 用于将容器的内容限制为指定的行数,也就是我们常实现的内容多时显示省略号的效果。

举个例子:

p {
  width: 300px;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

效果如下:

之所以被大家吐槽,多是因为技术局限性问题,比如:

  • 能限制行数,无法精确控制高度
  • 浏览器兼容性还不够好
  • 与动态内容配合困难:当文本内容长度不确定时,难以准确控制显示效果

当我们实际使用 line-clamp 的时候,还要配合一系列属性比如 display、-webkit-box-orient、overflow、text-overflow,这种组合方案既复杂又不够语义化。

7. 结论

CSS 这些年无疑在快速的发展中,而人们对 CSS 的满意度也在持续攀升。

引用报告中的一句话:

“如果说 2025 年的主题是稳定不可能之事,那么 2026 年或许是实现期待已久的梦想之年。”

对于热爱 CSS 的人来说,现在正是尝试、学习并参与塑造未来发展方向的最佳时机。

我是冴羽,10 年笔耕不辍,专注前端领域,更新了 10+ 系列、300+ 篇原创技术文章,翻译过 Svelte、Solid.js、TypeScript 文档,著有小册《Next.js 开发指南》、《Svelte 开发指南》、《Astro 实战指南》。

欢迎围观我的“网页版朋友圈”,关注我的公众号:冴羽(或搜索 yayujs),每天分享前端知识、AI 干货。

新的一年,如果你想快速改变自己,欢迎加入我的知识星球:“冴羽·前端大佬成长之路”,10 年工作总结、100+ 篇精华主题、70W 字原创内容,带你升级认知、重构生活、建立知识管理系统、通关面试、引领职场。用一年时间,实现十倍成长,一鸣惊人。

CSS-HTML Form 表单交互深度指南

2026年1月21日 16:15

前言

虽然现代前端框架(Vue/React)已经极大简化了表单处理,但理解原生 Form 表单 的事件流、控件属性和 API,依然是处理复杂业务逻辑(如埋点、自定义验证、无刷新提交)的基础。

一、 Form 表单的核心机制

<form> 是所有输入控件的容器。它不仅负责数据的收集,还管理着数据的提交 (Submit)重置 (Reset) 周期。

1. 关键属性

  • action:数据提交的目标 URL。
  • method:HTTP 请求方式(GET 拼接到 URL,POST 放入请求体)。

2. 提交与拦截

当表单内存在 type="submit" 的按钮时,点击会触发 submit 事件。

const form = document.querySelector("#myForm");

form.addEventListener("submit", (event) => {
  // 1. 阻止浏览器默认的跳转刷新行为
  event.preventDefault(); 
  
  // 2. 自定义验证逻辑
  if (inputValue === "") {
    alert("内容不能为空");
    return;
  }
  
  // 3. 执行异步提交(如使用 Fetch/Axios)
  console.log("表单已提交");
});

3. 重置行为

form.reset() 不仅仅是清空。它会将所有字段恢复为页面加载时的初始值(例如 <input value="default"> 会恢复为 "default" 而非空)。


二、 输入控件的“通用武器库”

无论 inputselect 还是 textarea,它们都共享以下核心属性和方法:

1. 公共属性与方法

  • disabled:禁用控件,数据不会被提交。
  • readOnly:只读,数据随表单提交。
  • form:只读属性,返回当前控件所属的表单对象引用。
  • focus() / blur() :手动控制焦点的获取与失去。

2. 三大核心事件

事件 触发时机
focus 控件获得焦点时。
blur 控件失去焦点时(常用于输入后的实时验证)。
change 内容改变且失去焦点时触发(注意:与 input 事件实时触发不同)。

三、 文本输入:Input vs Textarea

1. 单行文本框 <input type="text">

  • placeholder:提示文本。
  • maxlength:硬性限制用户输入的字符长度。

2. 多行文本框 <textarea>

  • rows/cols:控制显示的行数和宽度。

  • wrap 换行控制

    • soft(默认):提交时不带换行符。
    • hard:提交的数据中包含换行符(必须配合 cols 使用)。

四、 选择框:Select 与 Option

<select> 元素在 JavaScript 中拥有更丰富的集合操作。

1. Select 关键操作

  • multiple:是否允许多选(按住 Ctrl/Command 键)。
  • options:返回包含所有 <option> 元素的 HTMLCollection。
  • remove(index) :移除指定索引的选项。

2. Option 选项详情

每一个 Option 对象都有:

  • index:在下拉列表中的位置。
  • selected:布尔值,通过设置为 true 可实现代码控制选中。
  • text:用户看到的文字。
  • value:提交给后端的值。

五、 面试模拟题

Q1:submit 事件和按钮的 click 事件有什么区别?

参考回答:

submit 事件是绑定在 form 元素上的。如果用户在输入框中按“回车键”,或者点击了 type="submit" 的按钮,都会触发 form 的 submit 事件。相比点击事件,监听 submit 能更全面地捕获提交动作。

Q2:如何通过原生 JS 获取表单内的所有数据?

参考回答:

现代浏览器推荐使用 FormData 对象:

const formData = new FormData(formElement);
// 获取特定字段
const username = formData.get('username');
// 转化为对象
const data = Object.fromEntries(formData.entries());

Q3:disabledreadOnly 在表单提交时有什么区别?

参考回答:

  • 设置了 disabled 的控件,其值在表单提交时会被忽略,且用户无法交互。
  • 设置了 readOnly 的控件,用户无法修改值,但其值在提交时会被包含在表单数据中。

掌握 CSS 布局基石:行内、块级、行内块元素深度解析

2026年1月21日 14:43

前言

在 CSS 世界中,每个元素都有一个默认的 display 属性。理解这些元素的显示模式,是解决“为什么我的宽高设置无效?”、“为什么两个 div 不在一行?”等问题的关键。

一、 三大元素显示模式对比

1. 块级元素 (Block Elements)

块级元素就像是积木,默认从上往下堆叠。

  • 特点

    • 独占一行:默认占满父容器 100% 宽度。
    • 属性全开:支持设置 widthheightmarginpadding
    • 嵌套规则:可以包含行内元素和其他块级元素(注意:ph1~h6 比较特殊,建议不要包裹块级元素)。
  • 代表标签div, p, h1~h6, ul, ol, li, header, footer, section 等。

2. 行内元素 (Inline Elements)

行内元素就像是文本,随内容流动。

  • 特点

    • 并排显示:相邻元素在同一行内排列,直到排不下才换行。
    • 宽高无效:设置 widthheight 不起作用,宽度由内容撑开。
    • 间距局限:水平方向的 marginpadding 有效;垂直方向无效(不占据空间,但可能背景会溢出)。
  • 代表标签span, a, strong, em, i, label

3. 行内块元素 (Inline-Block)

结合了前两者的优点,既能并排显示,又能设置宽高。

  • 特点

    • 并排排列:不独占一行。
    • 属性支持:支持设置 widthheightmarginpadding
  • 代表标签img, input, button, textarea, select

    :这些元素在 CSS 规范中被称为“可替换元素”,它们天生具有行内块的特性。


二、 inline-block 的“间隙之谜”

1. 产生原因

当你给子元素设置 display: inline-block 时,HTML 代码中标签之间的空格或换行符会被浏览器解析为一个约 4px 的空白字符。

2. 解决方案

  • 方法 A:父元素设置 font-size: 0(最常用)

    .parent { font-size: 0; }
    .child { display: inline-block; font-size: 14px; } /* 子元素需手动恢复字号 */
    
  • 方法 B:标签首尾相接(代码极丑,不推荐)

    <div class="child">A</div><div class="child">B</div>
    
  • 方法 C:改用 Flex 布局(现代开发首选)

    .parent { display: flex; } /* 彻底告别间隙问题 */
    

三、 空元素 (Void Elements)

空元素是指没有子节点且没有结束标签的元素,它们通常通过属性来承载内容。

  • 常见标签<br>, <hr>, <img>, <input>, <link>, <meta>

四、 面试模拟题

Q1:如何让行内元素(如 span)支持宽高?

参考回答:

  1. 修改 display 属性为 blockinline-block
  2. 设置 float(浮动后的元素会自动变为块级表现)。
  3. 设置 position: absolutefixed

Q2:img 标签是行内元素还是块级元素?为什么它可以设置宽高?

参考回答: img 在表现上属于行内元素(不换行),但它是一个可替换元素(Replaced element) 。可替换元素的内容不受 CSS 控制,其外观由标签属性决定。浏览器在渲染这类元素时,会赋予它们类似 inline-block 的特性,因此可以设置宽高。

Q3:display: nonevisibility: hidden 有什么区别?

参考回答:

  • display: none:脱离文档流,不占据空间,会引起回流(Reflow)。
  • visibility: hidden:隐藏内容,但保留占据的物理空间,不会引起回流,仅引起重绘(Repaint)。
昨天以前首页

Tailwind CSS + lucide-react:手搓一个能打的产品级登录页

2026年1月18日 01:06

登录页这玩意儿,表面看就是两个输入框加按钮,但写过的人都知道——它简直是前端工程的“照妖镜”。组件抽象、状态管理、响应式、加载态、可访问性,全在这方寸之间。今天就把我踩过的坑、验证过的最佳实践,完整复盘一遍。


技术选型:为什么不是“全家桶”而是“三剑客”?

Vite:别用 CRA 了,真的

2024 年还用 CRA 新建项目,就像今天还在用 jQuery 写新需求——不是不行,只是没必要。Vite 的秒级启动、按需热更新、对 React 的丝滑支持,用了就回不去。

npm init vite@latest login-demo -- --template react

Tailwind CSS:原子化 CSS 的“真香”现场

刚开始我也抵触过——“这不就是 inline style 吗?”用了三个月后发现,Tailwind 的精髓在于用约束换自由

  • 不再纠结 .login-input--error 还是 .login__input-error
  • 样式紧耦合组件,重构时删组件即删样式,不留垃圾
  • 设计系统(间距、色板、圆角)被工具类强制约束,UI 一致性自然来

lucide-react:图标库的“现代化”答案

放弃 iconfont 吧,字体图标在 Retina 屏下糊、在 SSR 场景下闪、在暗黑模式里要单独维护反色。lucide-react 的 SVG 组件化方案:

  • Tree-shaking,只打包用到的图标,体积 < 10KB
  • 可传 size, className, strokeWidth,和普通组件无异
  • 和 Tailwind 的 text-*, fill-* 类无缝配合
import { Lock, Mail, Eye, EyeOff, Loader2 } from 'lucide-react';

登录页的业务复杂度:UI 只是冰山一角

真正落地的登录页至少包含:

  • 受控组件:杜绝 document.getElementById,数据单一源
  • 表单校验:实时反馈、错误聚合、防抖提交
  • 加载态:按钮禁用、节流、异步反馈
  • 密码显隐:无障碍支持(aria-label
  • 响应式:移动端优先的触控体验
  • 自动填充:处理浏览器自动填充的黄色背景
  • 安全:防止 XSS、CSRF Token 透传

React 状态设计:别写“面条代码”

1. 状态聚合:一个对象管所有

新手容易写成这样:

const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [rememberMe, setRememberMe] = useState(false);

维护过老代码的都知道,新增一个字段要改三行,提交时要拼半天对象。正确姿势:

const [form, setForm] = useState({ email: '', password: '', rememberMe: false });
const [errors, setErrors] = useState({});
const [ui, setUi] = useState({ loading: false, showPassword: false });

三层状态分离:数据层(form)、校验层(errors)、UI 层(ui)。各司其职,后续维护一目了然。

2. 表单处理的“万能钥匙”

const handleChange = (e) => {
  const { name, value, type, checked } = e.target;
  
  setForm(prev => ({
    ...prev,
    [name]: type === 'checkbox' ? checked : value
  }));
  
  // 实时清错
  if (errors[name]) {
    setErrors(prev => ({ ...prev, [name]: '' }));
  }
};

关键点:

  • 利用 name 属性做映射,扩展新字段零成本
  • 输入即清错,用户体验细节
  • 支持 text, password, checkbox, select 等所有表单元素

3. 提交逻辑的“防御性编程”

const handleSubmit = async (e) => {
  e.preventDefault();
  
  const nextErrors = validate(form);
  if (Object.keys(nextErrors).length) {
    setErrors(nextErrors);
    return;
  }
  
  setUi(prev => ({ ...prev, loading: true }));
  try {
    await onLogin(form); // 业务注入
  } catch (err) {
    setErrors({ form: err.message });
  } finally {
    setUi(prev => ({ ...prev, loading: false }));
  }
};

记住:loading 态必须在 finally 里关闭 ,无论成功失败,用户都要有反馈。


Tailwind 的工程化细节:不是堆砌,是设计

1. 容器:响应式的“黄金分割”

<div className="w-full max-w-md mx-auto px-6 py-8 md:px-10 md:py-12">
  • w-full max-w-md:移动端 100%,PC 端最大 448px
  • mx-auto:居中,无需额外写 margin: 0 auto
  • px-6 md:px-10:断点平滑过渡,避免“跳变”

2. 表单间距:space-y 的魔法

<form className="space-y-6" onSubmit={handleSubmit}>
  <div>...</div>
  <div>...</div>
  <button>...</button>
</form>

space-y-6 自动给每个子元素加 margin-top,除了第一个。等价于:

.form > * + * {
  margin-top: 1.5rem;
}

但语义更清晰,且避免了 first:mt-0 这类修补。

3. 输入框:状态即 class

<input
  className={clsx(
    "w-full rounded-lg border px-10 py-3 text-base transition",
    "border-slate-300 bg-white placeholder:text-slate-400",
    "focus:border-indigo-600 focus:ring-2 focus:ring-indigo-600/20 focus:outline-none",
    errors.email && "border-red-500 ring-2 ring-red-500/20"
  )}
/>
  • 利用 clsxclassNames 做条件合并
  • 聚焦态、错误态、默认态全用 class 表达
  • focus:outline-none 移除默认蓝框,用 ring 替代,更可控

4. 图标定位:group 的联动

<div className="relative group">
  <Mail className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 group-focus-within:text-indigo-600" />
  <input className="pl-10 ..." />
</div>
  • group-focus-within:父级聚焦,图标变色
  • pointer-events-none:让点击穿透到 input
  • 无需手写 :focus-within 选择器

lucide-react:图标是组件,不是字体

密码显隐:带无障碍支持的完整实现

<button
  type="button"
  onClick={() => setUi(prev => ({ ...prev, showPassword: !ui.showPassword }))}
  aria-label={ui.showPassword ? '隐藏密码' : '显示密码'}
  className="absolute right-3 top-1/2 -translate-y-1/2 p-1.5 rounded-md hover:bg-slate-100 transition"
>
  {ui.showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>

要点:

  • aria-label 读屏软件可读
  • type="button" 防止触发提交
  • hover:bg-slate-100 增加触控反馈

加载态:图标即动画

{ui.loading && <Loader2 className="mr-2 inline-block animate-spin" />}

animate-spin 是 Tailwind 内置动画,配合 lucide 的 Loader2 图标,无需额外写 CSS 动画。


响应式与暗黑模式:一次写好,到处适用

Mobile First 的触控优化

jsx

Copy

<input className="min-h-[44px] ..." /> {/* iOS 最小触控高度 */}
<button className="min-h-[44px] active:scale-95 ..."> {/* 按下反馈 */}

暗黑模式:Tailwind 的 dark 前缀

<div className="bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100">
  <Sun className="dark:hidden" />
  <Moon className="hidden dark:block" />
</div>

只需在 tailwind.config.js 里启用 darkMode: 'class',然后在顶层加 dark 类即可。


效果图

image.png


最后

登录页是前端工程师的“基本功”,也是“面试常考题”。把状态设计、Tailwind 原子类、可访问性、加载态这些细节处理好,后续复杂业务才能游刃有余。

记住:代码是写给下一个维护你的人看的,包括三个月后的自己。

前端样式工程化三剑客: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 性能开销较小,但大量或嵌套使用可能引发布局抖动,尤其在低端设备上。保持简洁,只在必要处使用。


📚 参考资料

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

❌
❌