普通视图

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

CSS容器查询:响应式设计的新范式

作者 bluceli
2026年3月9日 13:04

引言

在响应式设计的发展历程中,我们长期依赖媒体查询来根据视口大小调整布局。然而,现代Web应用的组件化特性使得基于视口的响应式设计显得力不从心。CSS容器查询的出现,为我们提供了一种基于容器尺寸而非视口尺寸的响应式设计新范式。本文将深入探讨CSS容器查询的核心概念、实际应用和最佳实践。

容器查询基础

1. 什么是容器查询

容器查询允许我们根据元素的容器尺寸来应用样式,而不是整个视口。这使得组件能够根据其所在容器的大小自适应调整,真正实现组件级别的响应式设计。

/* 定义容器 */
.card-container {
  container-type: inline-size;
  container-name: card;
}

/* 使用容器查询 */
@container card (min-width: 400px) {
  .card {
    display: grid;
    grid-template-columns: 1fr 1fr;
  }
}

2. 容器查询的基本语法

容器查询的语法与媒体查询类似,但使用@container规则。

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

/* 容器查询示例 */
@container (min-width: 300px) {
  .widget {
    flex-direction: row;
  }
}

@container (min-width: 500px) {
  .widget {
    padding: 20px;
  }
}

容器类型与命名

3. 容器类型详解

CSS提供了两种容器类型:sizeinline-size

/* size容器 - 同时跟踪内联和块级尺寸 */
.gallery {
  container-type: size;
}

/* inline-size容器 - 只跟踪内联尺寸(推荐) */
.card-wrapper {
  container-type: inline-size;
}

/* 为什么推荐inline-size */
/* 1. 性能更好:浏览器只需要跟踪一个维度 */
/* 2. 避免循环依赖:减少布局计算复杂度 */

4. 容器命名

为容器命名可以创建更精确的查询规则。

/* 定义命名容器 */
.main-content {
  container-type: inline-size;
  container-name: main;
}

.sidebar {
  container-type: inline-size;
  container-name: side;
}

/* 针对不同容器的查询 */
@container main (min-width: 600px) {
  .article {
    font-size: 18px;
  }
}

@container side (min-width: 300px) {
  .widget {
    display: block;
  }
}

实际应用案例

5. 响应式卡片组件

创建一个根据容器大小自适应的卡片组件。

/* 卡片容器 */
.card-container {
  container-type: inline-size;
  container-name: card;
}

/* 基础卡片样式 */
.card {
  background: white;
  border-radius: 8px;
  padding: 16px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* 小容器 - 垂直布局 */
@container card (max-width: 300px) {
  .card {
    display: flex;
    flex-direction: column;
  }
  
  .card-image {
    width: 100%;
    height: 150px;
  }
  
  .card-content {
    margin-top: 12px;
  }
}

/* 中等容器 - 水平布局 */
@container card (min-width: 301px) and (max-width: 500px) {
  .card {
    display: flex;
    flex-direction: row;
    gap: 16px;
  }
  
  .card-image {
    width: 120px;
    height: 120px;
    flex-shrink: 0;
  }
}

/* 大容器 - 网格布局 */
@container card (min-width: 501px) {
  .card {
    display: grid;
    grid-template-columns: 200px 1fr;
    gap: 20px;
  }
  
  .card-image {
    width: 100%;
    height: 200px;
    object-fit: cover;
  }
}

6. 自适应导航菜单

创建一个根据可用空间调整的导航菜单。

/* 导航容器 */
.nav-container {
  container-type: inline-size;
  container-name: nav;
}

/* 基础导航样式 */
.nav {
  display: flex;
  gap: 8px;
}

.nav-item {
  padding: 8px 16px;
  border-radius: 4px;
  transition: all 0.2s;
}

/* 窄容器 - 垂直菜单 */
@container nav (max-width: 400px) {
  .nav {
    flex-direction: column;
  }
  
  .nav-item {
    width: 100%;
    text-align: center;
  }
}

/* 中等容器 - 紧凑水平菜单 */
@container nav (min-width: 401px) and (max-width: 600px) {
  .nav-item {
    padding: 6px 12px;
    font-size: 14px;
  }
}

/* 宽容器 - 完整菜单 */
@container nav (min-width: 601px) {
  .nav {
    justify-content: space-between;
  }
  
  .nav-item {
    padding: 10px 20px;
  }
}

7. 响应式数据表格

创建一个根据容器宽度调整的表格。

/* 表格容器 */
.table-container {
  container-type: inline-size;
  container-name: table;
  overflow-x: auto;
}

/* 基础表格样式 */
.data-table {
  width: 100%;
  border-collapse: collapse;
}

.data-table th,
.data-table td {
  padding: 12px;
  text-align: left;
  border-bottom: 1px solid #e5e7eb;
}

/* 窄容器 - 紧凑表格 */
@container table (max-width: 500px) {
  .data-table th,
  .data-table td {
    padding: 8px;
    font-size: 14px;
  }
  
  .data-table .description {
    display: none;
  }
}

/* 中等容器 - 标准表格 */
@container table (min-width: 501px) and (max-width: 800px) {
  .data-table th,
  .data-table td {
    padding: 10px 14px;
  }
}

/* 宽容器 - 宽松表格 */
@container table (min-width: 801px) {
  .data-table th,
  .data-table td {
    padding: 16px 20px;
  }
  
  .data-table {
    font-size: 16px;
  }
}

高级技巧

8. 容器查询单位

使用容器查询单位(cqwcqhcqmincqmax)创建相对尺寸。

.product-card {
  container-type: size;
  container-name: product;
}

/* 使用容器查询单位 */
@container product {
  .product-image {
    width: 50cqw;  /* 容器宽度的50% */
    height: 30cqh; /* 容器高度的30% */
  }
  
  .product-title {
    font-size: 2cqw;  /* 相对于容器宽度的字体大小 */
  }
  
  .badge {
    width: min(10cqw, 100px);  /* 结合min()函数 */
    height: min(10cqw, 100px);
  }
}

9. 嵌套容器查询

在嵌套结构中使用多层容器查询。

/* 外层容器 */
.dashboard {
  container-type: inline-size;
  container-name: dashboard;
}

/* 内层容器 */
.widget {
  container-type: inline-size;
  container-name: widget;
}

/* 外层容器查询 */
@container dashboard (min-width: 800px) {
  .dashboard {
    display: grid;
    grid-template-columns: 250px 1fr;
  }
}

/* 内层容器查询 */
@container widget (min-width: 300px) {
  .widget-content {
    display: grid;
    grid-template-columns: 1fr 1fr;
  }
}

10. 容器查询与JavaScript结合

使用JavaScript动态控制容器查询。

// 检测容器查询支持
class ContainerQueryDetector {
  constructor() {
    this.supported = CSS.supports('container-type', 'inline-size');
  }
  
  init() {
    if (!this.supported) {
      this.loadPolyfill();
    }
  }
  
  loadPolyfill() {
    // 加载容器查询polyfill
    const script = document.createElement('script');
    script.src = 'https://unpkg.com/container-query-polyfill';
    document.head.appendChild(script);
  }
  
  // 动态设置容器
  setContainer(element, name, type = 'inline-size') {
    if (this.supported) {
      element.style.containerType = type;
      element.style.containerName = name;
    } else {
      // 回退方案
      element.classList.add(`container-${name}`);
    }
  }
}

// 使用示例
const detector = new ContainerQueryDetector();
detector.init();

// 动态创建容器
const cardContainer = document.querySelector('.card-container');
detector.setContainer(cardContainer, 'card');

性能优化

11. 容器查询性能考虑

容器查询的性能优化策略。

/* 1. 优先使用inline-size而非size */
.efficient-container {
  container-type: inline-size; /* 更好的性能 */
}

/* 2. 避免过度嵌套 */
/* 不推荐 */
.outer {
  container-type: inline-size;
}
.middle {
  container-type: inline-size;
}
.inner {
  container-type: inline-size;
}

/* 推荐 - 扁平化结构 */
.outer {
  container-type: inline-size;
}

/* 3. 合并查询规则 */
/* 不推荐 */
@container (min-width: 300px) {
  .item { padding: 10px; }
}
@container (min-width: 300px) {
  .item { margin: 10px; }
}

/* 推荐 */
@container (min-width: 300px) {
  .item {
    padding: 10px;
    margin: 10px;
  }
}

12. 渐进增强策略

为不支持容器查询的浏览器提供回退方案。

/* 基础样式 - 所有浏览器 */
.card {
  display: flex;
  flex-direction: column;
  padding: 16px;
}

/* 媒体查询回退 */
@media (min-width: 768px) {
  .card {
    flex-direction: row;
  }
}

/* 容器查询 - 现代浏览器 */
@supports (container-type: inline-size) {
  .card-container {
    container-type: inline-size;
  }
  
  @container (min-width: 400px) {
    .card {
      flex-direction: row;
    }
  }
}

实际项目应用

13. 电商产品卡片

完整的电商产品卡片实现。

/* 产品卡片容器 */
.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 20px;
}

.product-card {
  container-type: inline-size;
  container-name: product;
  background: white;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

/* 产品图片 */
.product-image {
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  transition: transform 0.3s;
}

.product-card:hover .product-image {
  transform: scale(1.05);
}

/* 产品信息 */
.product-info {
  padding: 16px;
}

.product-title {
  font-size: 16px;
  font-weight: 600;
  margin-bottom: 8px;
}

.product-price {
  font-size: 18px;
  font-weight: 700;
  color: #e53e3e;
}

/* 容器查询变体 */
@container product (min-width: 300px) {
  .product-title {
    font-size: 18px;
  }
  
  .product-price {
    font-size: 20px;
  }
}

@container product (min-width: 400px) {
  .product-info {
    padding: 20px;
  }
  
  .product-title {
    font-size: 20px;
  }
  
  .product-description {
    display: block;
    margin-top: 8px;
    color: #666;
    font-size: 14px;
  }
}

14. 响应式文章布局

自适应的文章内容布局。

/* 文章容器 */
.article-wrapper {
  container-type: inline-size;
  container-name: article;
  max-width: 100%;
}

/* 文章内容 */
.article-content {
  line-height: 1.8;
  color: #333;
}

.article-content h2 {
  font-size: 24px;
  margin: 32px 0 16px;
}

.article-content p {
  margin-bottom: 16px;
}

/* 容器查询适配 */
@container article (max-width: 400px) {
  .article-content {
    font-size: 16px;
  }
  
  .article-content h2 {
    font-size: 20px;
  }
  
  .article-content img {
    width: 100%;
    height: auto;
  }
}

@container article (min-width: 401px) and (max-width: 700px) {
  .article-content {
    font-size: 17px;
  }
  
  .article-content h2 {
    font-size: 22px;
  }
  
  .article-content img {
    max-width: 100%;
  }
}

@container article (min-width: 701px) {
  .article-content {
    font-size: 18px;
    max-width: 650px;
    margin: 0 auto;
  }
  
  .article-content h2 {
    font-size: 26px;
  }
  
  .article-content img {
    max-width: 100%;
    border-radius: 8px;
  }
}

调试与测试

15. 容器查询调试工具

使用开发者工具调试容器查询。

// 容器查询调试器
class ContainerQueryDebugger {
  constructor() {
    this.containers = new Map();
  }
  
  // 注册容器
  registerContainer(element, name) {
    this.containers.set(name, element);
    this.addDebugOverlay(element, name);
  }
  
  // 添加调试覆盖层
  addDebugOverlay(element, name) {
    const overlay = document.createElement('div');
    overlay.style.cssText = `
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      border: 2px dashed #ff6b6b;
      pointer-events: none;
      z-index: 9999;
    `;
    
    const label = document.createElement('div');
    label.textContent = name;
    label.style.cssText = `
      position: absolute;
      top: -20px;
      left: 0;
      background: #ff6b6b;
      color: white;
      padding: 2px 8px;
      font-size: 12px;
      border-radius: 4px;
    `;
    
    overlay.appendChild(label);
    element.style.position = 'relative';
    element.appendChild(overlay);
    
    // 监听尺寸变化
    this.observeSize(element, name);
  }
  
  // 监听容器尺寸变化
  observeSize(element, name) {
    const resizeObserver = new ResizeObserver(entries => {
      for (const entry of entries) {
        const { width, height } = entry.contentRect;
        console.log(`容器 ${name} 尺寸:`, {
          width: Math.round(width),
          height: Math.round(height)
        });
      }
    });
    
    resizeObserver.observe(element);
  }
}

// 使用示例
const debugger = new ContainerQueryDebugger();
debugger.registerContainer(
  document.querySelector('.card-container'),
  'card'
);

总结

CSS容器查询为响应式设计带来了革命性的变化:

核心优势

  1. 组件级响应式:组件根据自身容器而非视口调整
  2. 更好的复用性:组件在任何容器中都能正确显示
  3. 减少媒体查询:不再依赖全局视口尺寸
  4. 更精确的控制:基于实际可用空间调整布局

最佳实践

  1. 优先使用inline-size:性能更好,避免循环依赖
  2. 合理命名容器:提高代码可读性和维护性
  3. 渐进增强:为旧浏览器提供回退方案
  4. 性能优化:避免过度嵌套和冗余查询

应用场景

  • 组件库开发:创建真正自适应的UI组件
  • 复杂布局:多列、网格等灵活布局
  • 内容适配:根据可用空间调整内容显示
  • 响应式设计:替代或补充传统媒体查询

容器查询代表了CSS响应式设计的未来方向。随着浏览器支持的不断完善,它将成为现代Web开发的标准工具。开始在你的项目中使用容器查询,体验组件级响应式设计的强大能力!


本文首发于掘金,欢迎关注我的专栏获取更多前端技术干货!

CSS 这些年都经历了什么?一次看懂 CSS 的演化史

2026年3月9日 10:52

很多前端在学习 CSS 时都会有一个疑问:

为什么 CSS 会有这么多东西?
css2、css3、less、scss、css-in-js、原子 css ……
看起来像是不断在“发明新轮子”。

其实如果把时间线拉长,你会发现这些技术并不是随机出现的,而是在解决 不同阶段的工程问题

从宏观来看,CSS 的发展基本围绕三件事:

  • 提高复用能力
  • 提高可维护性
  • 提高性能与开发效率

今天我们就从时间线的角度,一步一步看看 CSS 的演化。


一、最早的 CSS:让网页变好看

Q:CSS 最初是为了解决什么问题?

答案其实非常简单:

让 HTML 和样式分离。

在早期网页中,HTML 既负责结构,又负责样式,比如:

<font color="red" size="5">Hello</font>

这种写法的问题是:

  • HTML 变得非常混乱
  • 样式无法统一管理
  • 修改成本极高

于是 CSS 诞生了。

最早的 CSS 提供的能力主要包括:

  • 选择器
  • 字体样式
  • 颜色
  • 盒模型
  • float 布局
  • position 定位

例如:

.container {
  width: 960px;
  margin: auto;
}

这一阶段的核心目标只有一个:

让网页可以优雅地控制样式。


二、CSS 变复杂之后:预处理器时代

随着网站越来越复杂,一个问题开始显现:

CSS 写起来越来越痛苦。

典型问题包括:

  • 没有变量
  • 代码重复
  • 结构混乱
  • 无法复用

于是出现了 CSS 预处理器

比较常见的有:

  • Less
  • Sass
  • SCSS

Q:预处理器解决了什么问题?

答案是:

让 CSS 拥有编程能力。

例如变量:

$primary: #1890ff;

button {
  color: $primary;
}

嵌套结构:

.card {
  padding: 20px;

  .title {
    font-size: 20px;
  }
}

Mixin 复用:

@mixin flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

预处理器的出现,让 CSS 第一次具备了 工程化能力

但问题依然存在。


三、CSS 的大问题:全局污染

当项目规模继续扩大时,一个新的问题开始困扰开发者:

CSS 是全局的。

例如:

.title {
  color: red;
}

如果项目中有多个 .title,样式就会互相污染。

于是社区提出了一种解决方案:

CSS Modules

Q:CSS Modules 是什么?

核心思想:

让 CSS 拥有局部作用域。

例如:

.title {
  color: red;
}

编译后会变成:

.title__3x82a

在 React 中使用:

import styles from "./index.module.css"

<div className={styles.title}></div>

这样就避免了:

  • 命名冲突
  • 全局污染

但 CSS Modules 仍然存在一个问题:

样式和组件仍然是分离的。


四、组件化时代:CSS-in-JS

当 React 等组件框架流行起来之后,开发者开始思考一个问题:

既然 UI 是组件,那 CSS 为什么不能也是组件的一部分?

于是出现了:

CSS-in-JS

典型写法:

const Button = styled.button`
  color: red;
  padding: 10px;
`

Q:CSS-in-JS 的优势是什么?

主要有三个:

1 作用域天然隔离

组件之间不会污染。

2 动态样式非常容易

const Button = styled.button`
  color: ${props => props.primary ? "blue" : "gray"};
`

3 完美适配组件化开发

样式和组件写在一起。

但 CSS-in-JS 也带来了新的问题:

  • 运行时性能开销
  • JS 体积变大
  • SSR 更复杂

于是前端社区又走向了另一条路线。


五、效率革命:原子 CSS

最近几年非常流行的一种方案叫:

Atomic CSS(原子 CSS)

核心思想是:

一个 class 只做一件事。

例如:

<div class="flex items-center justify-center p-4 text-red-500"></div>

每个 class 都是一个最小样式单元。

Q:原子 CSS 的优点是什么?

1️⃣ 高复用 2️⃣ 几乎没有冗余 CSS 3️⃣ 开发效率非常高 4️⃣ 编译体积很小

典型框架包括:

  • Tailwind
  • UnoCSS
  • WindiCSS

很多现代项目都会使用这种方式。


六、CSS 本身也在进化

与此同时,CSS 标准本身也在不断增强。

例如:

  • Flexbox
  • Grid
  • CSS Variable
  • Container Query
  • Nesting

例如 CSS 变量:

:root {
  --primary: red;
}

button {
  color: var(--primary);
}

很多原本需要 Sass 才能实现的能力,现在 CSS 原生就能实现了


七、从宏观角度看 CSS 的发展

如果把这些技术放在一条时间线上,其实非常清晰。

第一阶段

解决 样式问题

目标:

让网页变好看


第二阶段

解决 代码问题

工具:

  • Less
  • Sass
  • SCSS

目标:

让 CSS 更容易维护


第三阶段

解决 规模问题

工具:

  • CSS Modules
  • BEM

目标:

大型项目不混乱


第四阶段

解决 组件化问题

工具:

  • CSS-in-JS

目标:

CSS 与组件绑定


第五阶段

解决 效率和性能问题

工具:

  • Atomic CSS

目标:

写得更快,体积更小


八、真正推动 CSS 发展的三股力量

如果站在更高层看,其实是三件事在推动 CSS 进化。

1 前端项目规模爆炸

过去的网站:

几十个页面。

现在的应用:

几千个组件。

CSS 必须工程化。


2 前端框架崛起

组件化开发成为主流。

UI 不再是页面,而是 组件树

CSS 也必须适应组件化。


3 性能需求越来越高

现代网站需要:

  • SEO
  • SSR
  • SSG
  • Core Web Vitals

CSS 体积和加载速度变得非常重要。


九、现代前端通常怎么用 CSS?

现在很多项目会使用这样的组合:

Tailwind + CSS Variable

或者:

Tailwind + CSS Modules

这样可以同时获得:

  • 开发效率
  • 组件隔离
  • 高性能

十、一个有趣的趋势

很多人没有意识到一件事:

CSS 其实正在慢慢“消失”。

未来的 UI 体系更可能是:

Design Token
+
Atomic Engine
+
Runtime Layout

样式不再是手写 CSS 文件,而是由系统自动生成。

CSS 的角色,正在从“写样式”变成:

驱动 UI 渲染的底层语言。


如果这篇文章对你有帮助,欢迎点赞、转发,让更多前端一起看懂 CSS 的进化之路。

昨天以前首页

使用 clip-path: shape() 创建 Squircle 形状

2026年3月8日 10:05

你是否厌倦了传统的方形和圆形元素?想要为你的网页或设计增添一些独特的曲线美?今天,就让我们一起来探索如何使用CSS的clip-path属性来创建一个时尚的 Squircle 形状吧!

什么是Squircle?

Squircle,顾名思义,是Square(方形)和Circle(圆形)的结合体。它既有方形的棱角感,又融入了圆形的柔和曲线,给人一种既现代又舒适的视觉感受。

核心思路

由于corner-shape(专门实现Squircle的属性)目前浏览器支持度有限,我们通过clip-path: shape()来模拟实现,核心是用CSS变量统一控制弧度,通过数学计算让边角的曲线过渡更自然,变量可直接用百分比或像素值,灵活度拉满。

完整实现代码

直接复制这段代码,就能快速实现基础的Squircle效果,关键变量--r可直接调整,0%为纯正方形,50%为极致的Squircle效果,中间数值可按需自定义。

/* 基础Squircle样式,直接复用 */
.squircle {
  --r: 50%; /* 控制弧度,0%=正方形,50%=Squircle,支持像素值如20px */
  --_r: clamp(0%,var(--r)/2,25%);
  --_v: calc(var(--_r)*(1 - sqrt(2)/4));
  --_p: calc(var(--_v) - var(--_r)/2);
  clip-path: shape(
    from var(--_v) var(--_p),
    curve to 50% 0 with var(--_r) 0,
    curve to calc(100% - var(--_v)) var(--_p) with calc(100% - var(--_r)) 0,
    curve to calc(100% - var(--_p)) var(--_v) with calc(100% - 2*var(--_p)) calc(2*var(--_p)),
    curve to 100% 50% with 100% var(--_r),
    curve to calc(100% - var(--_p)) calc(100% - var(--_v)) with 100% calc(100% - var(--_r)),
    curve to calc(100% - var(--_v)) calc(100% - var(--_p)) with calc(100% - 2*var(--_p)) calc(100% - 2*var(--_p)),
    curve to 50% 100% with calc(100% - var(--_r)) 100%,
    curve to var(--_v) calc(100% - var(--_p)) with var(--_r) 100%,
    curve to var(--_p) calc(100% - var(--_v)) with calc(2*var(--_p)) calc(100% - 2*var(--_p)),
    curve to 0 50% with 0 calc(100% - var(--_r)),
    curve to var(--_p) var(--_v) with 0 var(--_r),
    curve to var(--_v) var(--_p) with calc(2*var(--_p)) calc(2*var(--_p))
  );
}

Demo 地址:codepen.io/editor/aire…

对比演示:clip-path实现 vs corner-shape原生

为了让大家直观看到效果差异,我们做一个可交互的对比demo,一边是clip-path: shape()的模拟实现,一边是corner-shape的原生实现,还能通过滑块实时调整弧度,代码如下(可直接运行测试)。

.squircle {
  --r: 40%;
  --_r: clamp(0%,var(--r)/2,25%);
  --_v: calc(var(--_r)*(1 - sqrt(2)/4));
  --_p: calc(var(--_v) - var(--_r)/2);
        clip-path: shape(
    from var(--_v) var(--_p),
    curve to 50% 0 with var(--_r) 0,
    curve to calc(100% - var(--_v)) var(--_p) with calc(100% - var(--_r)) 0,
    curve to calc(100% - var(--_p)) var(--_v) with calc(100% - 2*var(--_p)) calc(2*var(--_p)),
    curve to 100% 50% with 100% var(--_r),
    curve to calc(100% - var(--_p)) calc(100% - var(--_v)) with 100% calc(100% - var(--_r)),
    curve to calc(100% - var(--_v)) calc(100% - var(--_p)) with calc(100% - 2*var(--_p)) calc(100% - 2*var(--_p)),
    curve to 50% 100% with calc(100% - var(--_r)) 100%,
    curve to var(--_v) calc(100% - var(--_p)) with var(--_r) 100%,
    curve to var(--_p) calc(100% - var(--_v)) with calc(2*var(--_p)) calc(100% - 2*var(--_p)),
    curve to 0 50% with 0 calc(100% - var(--_r)),
    curve to var(--_p) var(--_v) with 0 var(--_r),
    curve to var(--_v) var(--_p) with calc(2*var(--_p)) calc(2*var(--_p))
  );
}

.corner-shape {
  --r: 40%;
  border-radius: var(--r);
  corner-shape: squircle;
}

Demo 地址:codepen.io/editor/aire…

总结

这次的技巧核心是用clip-path: shape()弥补corner-shape的兼容性不足,通过 CSS 变量让 Squircle 效果的调节更简单,一行代码修改弧度,适配各类前端样式开发需求。

这种丝滑的圆角效果,用在按钮、卡片、头像等元素上,能让页面的视觉质感提升一个档次,大家赶紧把代码收藏起来,下次开发直接复用~

扩展阅读

CSS进阶: background-clip

作者 helloweilei
2026年3月4日 17:23

background-clip 是 CSS 中一个用于控制背景(背景颜色或背景图片)的显示范围的属性。简单来说,它可以决定背景是铺满整个盒子(包括边框)、只铺到边框内部,还是只铺到文字下方。

它的核心作用是限制背景的绘制区域

基本语法与三个主要属性值

1. border-box(默认值)

背景延伸到边框区域的下方(即背景会覆盖边框)。

  • 效果: 如果边框是半透明或点线样式,你能看到边框下面的背景。
  • 示例:
    .box {
        background-clip: border-box;
        /* 背景会铺满整个元素区域,包括边框部分 */
    }
    

image.png

2. padding-box

背景只延伸到内边距(padding)区域,边框下面没有背景

  • 效果: 背景在边框内部就停止了,边框保持纯色(通常是元素本身的背景色或透明)。
  • 示例:
    .box {
        background-clip: padding-box;
        /* 背景只铺到内边距边缘,边框区域无背景 */
    }
    

image.png

3. text(最炫酷、最常用)

将背景裁剪成文字的形态。

  • 效果: 背景只在文字的形状内显示,文字以外的区域背景不可见。
  • 关键配合: 通常需要配合 color: transparent 将文字颜色设为透明,才能看到被裁剪出来的背景。
  • 示例: 实现渐变文字、图片文字效果。
    .text {
        background-image: linear-gradient(45deg, #f00, #00f); /* 设置渐变背景 */
        color: transparent; /* 把文字本身的颜色变透明 */
        background-clip: text; /* 把背景裁剪成文字的形状 */
        -webkit-background-clip: text; /* 某些浏览器需要加前缀 */
    }
    

image.png

直观理解

想象一个带有内边距(padding)、边框(border)和背景色的盒子:

  • border-box:油漆刷满整个盒子,连边框(即使边框是虚线)也覆盖了背景色。
  • padding-box:油漆刷到边框内侧就停止了,边框区域没有油漆,保持原色。
  • text:油漆只涂在文字笔画上,其他地方(包括文字内部的镂空部分)都是透明的。

主要应用场景

  1. 渐变文字(最流行): 使用 background-clip: text 配合渐变色,制作醒目的标题。
  2. 特殊边框效果: 当希望边框是纯色,而背景在边框内部显示时(例如实现双层边框效果),可以使用 padding-box
  3. 精确控制背景平铺: 当不希望背景图延伸到边框下时,通过 padding-box 可以精确控制背景的边界。

浏览器兼容性

我们来具体看一下 background-clip 属性的浏览器兼容性情况。

总的来说,background-clip 的基础功能(border-boxpadding-boxcontent-box)兼容性非常好,可以放心使用。但它的“明星”功能 text 值兼容性稍复杂一些,需要特别注意写法。

我把它们的兼容性情况整理成了表格,方便你查看:

属性值 支持情况 主要细节 兼容性概览
基础值
(border-box, padding-box, content-box)
全面支持 所有现代浏览器及 IE9+ 均支持。 ✅ 很好
text 值
(background-clip: text)
广泛支持,但有细节 Chrome、Edge、Opera:从较早期版本就开始支持。
Safari:从 15.5 版本开始完全支持,早期版本(3.2-15.4)需加 -webkit- 前缀且为部分支持。
Firefox:从 49 版本开始支持,但早期版本(2-48)不支持。
Internet Explorer:全系不支持
移动端:主流浏览器(iOS Safari、Chrome for Android、Samsung Internet 等)基本都支持,但 Opera Mini 全系不支持。
🟡 良好,需注意

关键知识点与最佳实践

结合你之前问到的 background-clip 作用,这里有几个实践中的要点:

  1. text 值的标准写法 为了让 background-clip: text 在所有支持的浏览器上生效,必须同时使用带 -webkit- 前缀和不带前缀的写法。同时,记得将文字颜色设置为透明,背景图才能透出来。

    .gradient-text {
      background-image: linear-gradient(45deg, #ff6b6b, #4ecdc4);
      -webkit-background-clip: text; /* 为基于 WebKit 内核的浏览器添加 */
      background-clip: text;        /* 标准属性 */
      color: transparent;            /* 让文字颜色透明,露出背景 */
      -webkit-text-fill-color: transparent; /* 为 Safari 浏览器添加,增强兼容性 */
    }
    

    这里额外添加了 -webkit-text-fill-color: transparent,可以进一步增强在 Safari 等浏览器上的表现。

  2. Firefox 的特别注意事项 虽然 Firefox 从 49 版本开始支持 background-clip: text,但网上一些资料提到它在部分 Firefox 版本中可能存在问题,或者效果不如 Chrome/Safari 稳定。为了稳妥,可以结合 @supports 进行特性检测,为不支持(或支持不完美)的浏览器提供一个优雅的降级样式。

    .gradient-text {
      /* 默认样式(降级方案),比如一个纯色 */
      color: #ff6b6b;
    }
    
    /* 当浏览器支持 background-clip: text 时,应用渐变效果 */
    @supports (background-clip: text) or (-webkit-background-clip: text) {
      .gradient-text {
        background-image: linear-gradient(45deg, #ff6b6b, #4ecdc4);
        -webkit-background-clip: text;
        background-clip: text;
        color: transparent;
        -webkit-text-fill-color: transparent;
      }
    }
    
  3. 避开已知的坑

    • 不要只写不带前缀的属性:在现代浏览器中,仅写 background-clip: text 可能被忽略。
    • 背景必须用 background-image:使用渐变或图片,纯色背景无法体现裁切效果。
    • 留意边缘渲染:在一些非整数缩放比例或高分辨率屏幕上,文字边缘可能会出现轻微发虚或锯齿。通常使用稍粗一点的字体 (font-weight: 600 或更粗) 可以缓解。

总结一下,background-clip 的基础功能可以无忧使用。如果要用 text 值实现炫酷的文字效果,遵循上述的双前缀、透明文字和降级方案这“三板斧”,就能在绝大多数现代浏览器上获得理想且稳定的效果。

前端组件化样式隔离实战:React CSS Modules、styled-components 与 Vue scoped 对比

2026年3月3日 17:10

前端组件化样式隔离实战:React CSS Modules、styled-components 与 Vue scoped 对比

引言:当组件遇见 CSS

在现代前端开发中,组件化已成为构建用户界面的主流方式。我们将页面拆分为独立、可复用的组件,每个组件管理自己的 HTML、CSS 和 JavaScript。然而,CSS 的设计初衷是全局作用域的 —— 样式一旦定义,就会影响整个页面,这给组件化带来了严峻挑战。

试想一个多人协作的项目:A 同学写了一个按钮组件,类名为 .button;B 同学也写了一个按钮组件,同样用了 .button。当两个组件同时出现在页面上时,后加载的样式会覆盖前者,造成意料之外的 UI 错乱。如何让组件的样式“与世隔绝”,既不影响他人,也不被他人影响?本文将深入探讨 React 和 Vue 生态中三种主流的样式隔离方案:CSS Modulesstyled-componentsVue scoped。我们将通过实际代码,由浅入深地理解它们的原理与用法。

1. CSS 的“先天不足”与组件化的冲突

在传统网页开发中,我们通常这样写 CSS:

/* global.css */
.button {
  background-color: blue;
  color: white;
}

这个 .button 样式会作用于页面上所有带有 class="button" 的元素,无论它身处哪个组件。这种全局性在小型项目中或许无伤大雅,但在组件化架构下却成了灾难。

假设我们有 Button.jsxAnotherButton.jsx 两个组件,分别引入了各自的 CSS 文件:

/* Button.css */
.button { background: blue; }

/* AnotherButton.css */
.button { background: red; }
效果图

image.png

最终页面上两个按钮都会是红色,因为后引入的 AnotherButton.css 覆盖了前者的规则。这就是样式冲突的典型场景。

为了解决这一问题,社区发展出了多种作用域隔离方案,核心思想都是将样式“限定”在组件内部。下面我们分别看看 React 和 Vue 是如何做到的。

2. React 中的 CSS Modules

2.1 什么是 CSS Modules?

CSS Modules 是一种将 CSS 文件编译为局部作用域的技术。它并不是官方的 CSS 规范,而是通过构建工具(如 webpack)在编译时给类名自动添加唯一的哈希字符串,从而实现样式隔离。在 React 项目中,使用 Create React App 或 Vite 脚手架时,开箱即支持 CSS Modules。

2.2 基本用法

我们约定 CSS 文件命名为 *.module.css。在组件中像导入一个对象一样导入样式文件,然后通过对象的属性引用类名。

Button.module.css

.button {
  background-color: blue;
  color: white;
  padding: 10px 20px;
}

.txt {
  color: red;
  background-color: orange;
  font-size: 30px;
}

Button.jsx

import styles from './Button.module.css';
console.log(styles); // 输出:{ button: "Button_button__1a2b3c", txt: "Button_txt__4d5e6f" }

export default function Button() {
  return (
    <>
      <h1 className={styles.txt}>你好,世界!!!</h1>
      <button className={styles.button}>My Button</button>
    </>
  );
}
效果图

image.png

在浏览器中,最终渲染的 HTML 类似:

<h1 class="Button_txt__4d5e6f">你好,世界!!!</h1>
<button class="Button_button__1a2b3c">My Button</button>
打开控制台我们点击元素开可以看到每个元素都有唯一的id

image.png

可以看到,原始的类名 .button.txt 被转换成了带有组件名和哈希的唯一类名,从而避免了全局污染。

2.3 多人协作的保障

再来看另一个组件 AnotherButton,它也定义了同名的 .button 样式:

anotherButton.module.css

.button {
  background-color: red;
  color: black;
  padding: 10px 20px;
}

AnotherButton.jsx

import styles from './anotherButton.module.css';

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

两个组件的样式互不干扰,因为编译后的类名分别是 AnotherButton_button__xxxButton_button__xxx。这正是 CSS Modules 的魅力所在——让开发者无需担心类名冲突,专注于组件本身的样式。

2.4 原理浅析

CSS Modules 的原理并不复杂:在构建阶段,webpack 的 css-loader 会解析 *.module.css 文件,将每个类名映射为一个唯一的标识符(通常是 [文件名]_[类名]__[hash]),同时生成一个映射对象(即 styles)。在 JavaScript 中,我们通过这个映射对象来引用最终的类名,而 CSS 文件中的原始类名则被替换为哈希后的类名。这样,CSS 和 JS 就通过同一份映射关系保证了样式的私有性。

3. React 中的 styled-components

如果说 CSS Modules 是在编译时通过修改类名来实现隔离,那么 styled-components 则代表了另一种思潮:CSS-in-JS,即在 JavaScript 中编写 CSS,并利用 JavaScript 的作用域来实现样式隔离。

3.1 什么是 styled-components?

styled-components 是一个流行的 React 库,它允许你使用 ES6 的模板字符串定义样式组件,这些样式组件会自动生成一个唯一的类名,并将样式注入到 <head> 中。

3.2 基本用法

首先安装 styled-components:

npm install styled-components

然后在组件中创建样式化组件:

import styled from 'styled-components';

// 定义一个带样式的 button 组件
const Button = styled.button`
  background-color: ${props => props.primary ? 'blue' : 'white'};
  color: ${props => props.primary ? 'white' : 'blue'};
  border: 1px solid blue;
  padding: 8px 16px;
  border-radius: 4px;
`;

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

image.png 渲染后的 HTML 如下(每个人的截图中的真实类名可能不同):

<button class="sc-axZvf jflFSQ">默认按钮</button>
<button class="sc-axZvf efDizw">主要按钮</button>
打开控制台点开控制台元素,我们同样可以看到每个元素都有唯一id

image.png 这里的 sc-axZvf 是组件标识前缀,同一组件生成的实例共享这个前缀,而 jflFSQ 和 efDizw 则是具体的样式类名,分别对应不同的样式规则(例如一个是默认样式,一个是 primary 样式)。所有样式都被动态地生成为 <style> 标签插入页面头部。

3.3 动态样式与 props

styled-components 的一大优势是支持基于 props 的动态样式。如上例所示,通过 props.primary 可以轻松改变背景色和文字颜色。这比传统 CSS 需要额外维护多个类名要直观得多。

3.4 原理浅析

styled-components 在运行时(runtime)工作:当组件渲染时,它会解析模板字符串中的样式规则,根据 props 计算出最终的 CSS 文本,然后生成一个唯一的类名(如 jflFSQ),并将 CSS 规则以 <style> 标签的形式插入到文档头部。值得注意的是,同一组件(如 Button)的所有实例会共享一个组件级标识(sc-axZvf),而具体样式类名则每个实例或每个变体可能不同。由于每个组件实例都可能生成不同的类名,样式天然是隔离的。同时,它还能自动处理浏览器前缀、关键帧动画等,为开发者提供了良好的体验。

4. Vue 中的 scoped 样式

Vue 作为另一大前端框架,其单文件组件(SFC)提供了内置的样式隔离方案——scoped 属性。

4.1 什么是 scoped?

在 Vue 的单文件组件中,可以在 <style> 标签上添加 scoped 属性,指示该样式只作用于当前组件。它的实现方式是为组件模板中的元素添加唯一的自定义属性(如 data-v-xxxxx),然后通过属性选择器来限制样式的生效范围。每个组件会生成一个唯一的哈希 ID,该组件内的所有元素都会被打上这个 ID 作为属性。

4.2 基本用法

App.vue

<script setup>
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
  <div>
    <h1 class="txt">Hello world in App</h1>
    <h2 class="txt2">一点点</h2>
    <HelloWorld />
  </div>
</template>

<style scoped>
.txt {
  color: red;
}
.txt2 {
  color: green;
}
</style>

HelloWorld.vue

<template>
  <div>
    <h1 class="txt">你好,世界!!!</h1>
    <h2 class="txt2">一点点</h2>
  </div>
</template>

<style scoped>
.txt {
  color: blue;
}
.txt2 {
  color: orange;
}
</style>
效果图

image.png

4.3 渲染结果与原理

编译后,Vue 会为每个组件生成一个唯一的哈希 ID。假设 App 组件的 ID 为 data-v-7a7a37b1,HelloWorld 组件的 ID 为 data-v-e17ea971。最终渲染的 HTML 结构如下(来自实际截图):

html

<div data-v-7a7a37b1>
  <h1 data-v-7a7a37b1 class="txt">Hello world in App</h1>
  <h2 data-v-7a7a37b1 class="txt2">一点点</h2>
</div>

<div data-v-e17ea971 data-v-7a7a37b1>
  <h1 data-v-e17ea971 class="txt">你好,世界!!!</h1>
  <h2 data-v-e17ea971 class="txt2">一点点</h2>
</div>

仔细观察可以发现:

  • App 组件内的所有元素(包括根 div)都带有自己的 ID data-v-7a7a37b1
  • HelloWorld 组件内的所有元素(包括其根 div)都带有自己的 ID data-v-e17ea971特别地,HelloWorld 的根元素上还额外附加了父组件 App 的 ID data-v-7a7a37b1。这是 Vue 故意设计的,目的是让父组件的样式可以通过属性选择器(如 .txt[data-v-7a7a37b1])作用于子组件的根元素,从而实现父组件对子组件根节点的样式控制(如果父组件样式选择器匹配的话)。

对应的 CSS 会被编译为:

css

.txt[data-v-7a7a37b1] { color: red; }
.txt2[data-v-7a7a37b1] { color: green; }
.txt[data-v-e17ea971] { color: blue; }
.txt2[data-v-e17ea971] { color: orange; }

由于属性选择器的存在,每个组件的样式只作用于带有对应属性的元素,实现了完美的样式隔离。同时,子组件根元素拥有双重属性,使得父组件的样式能够有选择地影响子组件的最外层,保持了样式的可控性。

打开控制台元素,我们就可以看到

image.png

4.4 与 CSS Modules 的对比

Vue 的 scoped 与 React 的 CSS Modules 思路相似,都是通过给选择器附加唯一标识来实现作用域。区别在于:

  • CSS Modules 修改了类名本身,而 Vue 保留了原始类名,额外添加了属性选择器。
  • Vue 的 scoped 无需导入对象,直接在模板中使用原始类名,可读性更好。
  • CSS Modules 需要显式引用 styles 对象,略显繁琐,但胜在灵活(比如可以组合多个类名)。

4.5 原理浅析

Vue 在编译单文件组件时,会为每个组件生成一个唯一的哈希 ID。然后:

  1. 将模板中的所有元素加上该 ID 作为属性(根元素额外加上父组件的 ID,如果存在父组件)。
  2. 将 <style scoped> 中的每条 CSS 规则都加上对应的属性选择器。
  3. 最终生成带作用域的 CSS。

整个过程在构建阶段完成,没有运行时开销,性能极佳。

5. 对比与总结

方案 框架 实现原理 优点 缺点
CSS Modules React / Vue 编译时修改类名,生成哈希映射 静态样式,简单可靠;可与预处理器结合 类名需要引用,模板稍显啰嗦
styled-components React 运行时生成唯一类名,注入 <style> 动态样式能力强;完全组件化;支持 props 运行时开销;包体积较大;调试稍难
Vue scoped Vue 编译时添加唯一属性,属性选择器限制 语法简洁;无运行时开销;保留原始类名 仅适用于 Vue;深度选择器需特殊处理

如何选择?

  • 如果你的项目是 React,且偏好“传统”的 CSS 写法,CSS Modules 是最佳选择,它简单、高效,与设计工具(如 Figma)配合良好。
  • 如果你追求极致的动态样式和组件封装,或者希望将样式也作为组件逻辑的一部分,styled-components 能带来流畅的开发体验。
  • 对于 Vue 项目,scoped 是官方推荐的内置方案,开箱即用,足够满足绝大多数场景。

当然,这些方案并非互斥。在大型项目中,你可能会组合使用它们:用 CSS Modules 处理全局样式库,用 styled-components 处理高频复用的动态组件。重要的是理解每种方案的原理,以便在合适的场景做出正确的选择。

结语

从 CSS 的全局困境到组件样式的精细隔离,前端社区给出了多种优雅的解决方案。无论是 React 的 CSS Modules 和 styled-components,还是 Vue 的 scoped,它们都体现了“关注点分离”到“组件内聚”的思想演进。希望本文能帮助你更好地掌握这些工具,在项目中写出健壮、可维护的样式代码。如果你有更多关于样式隔离的思考或实践,欢迎在评论区交流讨论!

🚀 别再乱写 16px 了!CSS 单位体系已经进入“计算时代”,真正的响应式布局

作者 Sailing
2026年3月3日 13:50

如果你的项目里还充满 px + media query —— 那说明你在“维护样式”,而不是“设计系统”。

现代 CSS 的能力,已经远远超出“写几个断点”那么简单。

今天我们从工程视角,把 单位体系 + 计算体系 一次讲透。

u=3751345329,3281070072&fm=253&fmt=auto&app=120&f=JPEG.webp

CSS 单位体系:本质不是长度,而是“依赖关系”

CSS 单位可以分成三类:

  1. 绝对单位
  2. 相对单位
  3. 视口单位

单位能力对比表

单位 类型 依赖对象 系统角色 推荐级别 典型场景
px 绝对 精准控制 ✅ 必须存在 图标 / 1px 边框
em 相对 父级字体 局部缩放 ⚠️ 慎用 组件内部
rem 相对 根字体 全局缩放 ✅✅ 核心 系统布局
vw 视口 视口宽度 宽度适配 响应布局
vh 视口 视口高度 高度控制 全屏模块
vmin 视口 较小值 动态比例 特殊适配
vmax 视口 较大值 极端场景 特殊适配
dvw 动态视口 实际可视宽 移动端修正 🚀 H5
dvh 动态视口 实际可视高 移动端高度修正 🚀 APP/H5

重点认知

单位不是写数值
单位是在声明“依赖谁”

  • px → 不依赖任何人(绝对控制)
  • rem → 依赖根节点(系统级缩放)
  • vw → 依赖视口(设备相关)
  • dvh → 依赖真实可视区域(移动端优化)

如果你做企业级系统:

推荐核心组合

rem + clamp + min/max + dvh

其他单位只是辅助。我们往下看!

WX20240522-152437@2x.png

函数才是现代 CSS 的“计算引擎”

它们不是单位,但它们决定单位如何协作。

函数 作用 优势 工程价值
calc() 计算 混合单位运算 结构关系表达
min() 上限控制 自动封顶 替代 max-width
max() 下限控制 自动兜底 保证可读性
clamp() 区间控制 响应式缩放 替代 media query

核心函数深度解析(实战理解)

现在我们进入“可落地”部分。

🔥 1. rem —— 系统级缩放开关

原理

1rem = htmlfont-size

推荐做法

html {
  font-size: 16px;
}

组件写法:

.box {
  padding: 2rem;
}

为什么它是系统基石?

如果未来:

  • 设计改缩放比例
  • 项目整体要变大

你只需要改:

html { font-size: 18px }

🔥 全站自动缩放。
🔥 无需改任何组件。

这才叫“系统”。

🔥 2. clamp() —— 响应式终极武器

这是现代 CSS 的核心。

语法

clamp(min, preferred, max)

实战:

h1 {
  font-size: clamp(20px, 4vw, 48px);
}

效果:小屏不小于 20px,大屏不超过 48px,中间随视口自动变化。

工程价值

❌ 不需要维护设备列表 media query (@media (max-width: 768px))
❌ 不需要写多个断点
❌ 不需要拆分 PC / Mobile

✅ 一行表达“数学区间关系”

这不是技巧,是范式升级。

🔥 3. min() —— 自动封顶

传统写法:

width: 90%;
max-width: 1200px;

现代写法:

width: min(1200px, 90%);

区别?

  • 数学表达
  • 单行逻辑
  • 结构更清晰

表达的是:

宽度 = 两者中更小的那个

这才叫可维护。

🔥 4. max() —— 自动兜底

.box {
  padding: max(16px, 2vw);
}

保证:

  • 最小 16px
  • 又允许动态放大

用于保证阅读体验、可触控面积。

🔥 5. calc() —— 混合运算核心

.sidebar {
  width: 300px;
}

.content {
  width: calc(100% - 300px);
}

能力:

  • 支持加减乘除
  • 支持单位混合
  • 表达结构关系

它表达的是:

主体宽度 = 容器宽度 - 侧栏宽度

这叫“布局计算”,而不是“写死数值”。

39eb0728a2c0407faacb769863300d59.gif

视口单位:vw / vh / vmin / vmax

它们的共同点只有一个:依赖设备视口

vw

1vw = 视口宽度的 1%
.box {
  width: 50vw;
}

用于横向比例布局。

vh

1vh = 视口高度的 1%
.hero {
  height: 100vh;
}

用于全屏模块。

⚠️ 移动端慎用(dvh 更稳定)。

vmin

vmin = min(vw, vh)

始终基于短边。

.circle {
  width: 50vmin;
  height: 50vmin;
}

横竖屏切换比例稳定。

vmax

vmax = max(vw, vh)

始终基于长边。

.bg {
  font-size: 20vmax;
}

适合视觉冲击型页面。

image(2).png

移动端必须升级:dvh、dvw

移动端地址栏会动态伸缩,100vh ≠ 实际可视高度。

解决方案:

height: 100dvh;

优势:

  • 永远是真实可视区域
  • 不会因浏览器 UI 变化跳动
  • H5 / WebApp 必备

企业级布局推荐方案

如果你做后台系统 / 复杂管理台:

❌ 过时写法

  • 大量 @media
  • 到处 max-width
  • 写死 16px / 20px
  • 用断点区分设备

那是样式堆叠时代。

✅ 现代写法

固定系统:

px + min + max + clamp

弹性系统:

rem + clamp + vw + dvh

目标只有一个:写“关系”,而不是写“数值”。

总结:真正的思维升级

如果你的项目:

  • 还在到处写 16px
  • 还在疯狂加 media query
  • 还在拆 PC / 移动端
@media (max-width: 768px)

你是在:手动划分设备,维护设备列表,增加未来维护成本。

而当你写:

font-size: clamp(16px, 2vw, 24px);

你是在:写数学区间,写系统规则,让浏览器自己计算

你认为呢,希望这篇文章对你有所帮助、有所借鉴,欢迎在评论区随时沟通。

❌
❌