每天一个高级前端知识 - Day 5

0 阅读4分钟

每天一个高级前端知识 - Day 5

今日主题:现代CSS架构 - Container Queries、:has()、CSS嵌套彻底重塑组件样式

核心概念:CSS终于有了真正的逻辑作用域

长期以来,CSS缺少响应组件自身尺寸的能力,只能依赖@media(视口)。现代CSS三剑客彻底改变了这一局面。

🔍 Container Queries(容器查询)

解决的核心问题:组件在不同父容器中自适应样式,而非仅依据视口。

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

/* 查询容器宽度 */
@container card (min-width: 400px) {
  .card {
    display: flex;
    flex-direction: row;
  }
  
  .card img {
    width: 40%;
  }
}

@container card (max-width: 399px) {
  .card {
    display: flex;
    flex-direction: column;
  }
}

实战:自适应卡片组件

<div class="sidebar">
  <div class="card-container">
    <div class="card">
      <img src="avatar.jpg" />
      <h3>用户资料</h3>
      <p>侧边栏中的紧凑卡片</p>
    </div>
  </div>
</div>

<div class="main-content">
  <div class="card-container">
    <div class="card">
      <img src="avatar.jpg" />
      <h3>用户资料</h3>
      <p>主区域中的完整卡片</p>
    </div>
  </div>
</div>

🎭 :has() 父级选择器

解决的核心问题:终于可以根据子元素状态影响父元素!

/* 包含错误输入的字段组显示红色边框 */
.form-group:has(input:invalid) {
  border: 2px solid red;
  background-color: #fff0f0;
}

/* 包含图标的按钮增加内边距 */
button:has(svg) {
  padding: 12px 16px;
  display: inline-flex;
  gap: 8px;
}

/* 有缩略图的文章卡片使用网格布局 */
.article-card:has(img) {
  display: grid;
  grid-template-columns: 120px 1fr;
  gap: 16px;
}

/* 任何包含选中复选框的列表项高亮 */
li:has(input[type="checkbox"]:checked) {
  background-color: #e8f5e9;
  text-decoration: line-through;
}

📦 CSS嵌套(告别Sass)

/* 原生CSS嵌套 - 不再需要Sass! */
.card {
  padding: 1rem;
  
  /* 直接嵌套 */
  .card-header {
    font-weight: bold;
    
    /* & 引用父选择器 */
    &:hover {
      background-color: #f0f0f0;
    }
  }
  
  /* 媒体查询嵌套 */
  @media (min-width: 768px) {
    padding: 2rem;
  }
  
  /* & 可以放在任意位置 */
  body.dark-theme & {
    background-color: #1a1a1a;
  }
}

🚀 完整实战:响应式仪表盘组件

/* 仪表盘网格 - 使用容器查询实现自适应 */
.dashboard {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 1rem;
  container-type: inline-size;
  container-name: dashboard;
}

/* 统计卡片基类 */
.stats-card {
  container-type: inline-size;
  container-name: stats;
  padding: 1rem;
  border-radius: 0.75rem;
  background: white;
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  
  /* 默认:垂直布局 */
  .stats-content {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
  }
  
  /* 当卡片宽度超过200px时切换为水平 */
  @container stats (min-width: 200px) {
    .stats-content {
      flex-direction: row;
      align-items: center;
      justify-content: space-between;
    }
  }
}

/* 使用:has()实现智能高亮 - 悬停或包含特定数值 */
.stats-card:has(.value[data-status="warning"]) {
  border-left: 4px solid orange;
}

.stats-card:has(.value[data-status="critical"]):hover {
  background-color: #ffebee;
  transform: scale(1.02);
  transition: all 0.2s;
}

.stats-card:not(:has(.value)) {
  opacity: 0.6;
  filter: grayscale(0.3);
}

/* CSS嵌套 + 容器查询的组合 */
.dashboard {
  @container dashboard (min-width: 800px) {
    grid-template-columns: repeat(4, 1fr);
    
    .stats-card:first-child {
      grid-column: span 2;
      
      /* 嵌套内再嵌套 */
      @container stats (min-width: 300px) {
        .stats-content {
          flex-direction: row;
          
          .value {
            font-size: 3rem;
          }
        }
      }
    }
  }
}

💡 高级技巧:CSS作用域与样式隔离

/* @scope 规则 - 限制选择器的作用域 */
@scope (.component) to (.ignore) {
  /* 只在.component内生效,且不穿透.ignore */
  p {
    margin: 1em 0;
  }
  
  /* & 表示作用域根元素 */
  & > h2 {
    font-size: 1.5rem;
  }
}

/* 层级叠层 - 定义不同层的样式权重 */
@layer reset, theme, components, utilities;

@layer reset {
  * { margin: 0; padding: 0; box-sizing: border-box; }
}

@layer theme {
  :root { --primary: #0066cc; }
}

@layer components {
  .button {
    background: var(--primary);
    /* ... */
  }
}

@layer utilities {
  .flex { display: flex; }
  /* utilities 优先级最高 */
}

🎯 今日挑战

实现一个完全自适应的产品卡片网格,要求:

  1. 使用 Container Queries 让卡片根据自身容器宽度切换3种布局(紧凑/标准/详细)
  2. 使用 :has() 实现:卡片包含视频时显示播放图标,包含折扣标记时显示红色价格
  3. 使用原生 CSS 嵌套组织所有样式(不依赖预处理器)
  4. 实现暗色主题切换,同时使用容器查询保持自适应
参考实现(点击展开)
/* 核心实现思路 */
.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 1.5rem;
  container-type: inline-size;
  
  /* 暗色主题支持 */
  @media (prefers-color-scheme: dark) {
    background: #1a1a1a;
  }
}

.product-card {
  container-type: inline-size;
  container-name: product;
  border-radius: 0.75rem;
  overflow: hidden;
  transition: all 0.2s;
  
  /* 嵌套组织样式 */
  .card-content {
    padding: 1rem;
    
    .title {
      font-weight: 600;
      margin-bottom: 0.5rem;
    }
    
    .price {
      color: #0066cc;
      font-weight: 700;
    }
  }
  
  /* 紧凑布局 (< 200px) */
  @container product (max-width: 199px) {
    .card-content {
      .title { font-size: 0.875rem; }
      .description { display: none; }
      .price { font-size: 0.875rem; }
    }
  }
  
  /* 标准布局 (200px - 350px) */
  @container product (min-width: 200px) and (max-width: 349px) {
    .card-content {
      .title { font-size: 1rem; }
      .description { 
        display: -webkit-box;
        -webkit-line-clamp: 2;
      }
    }
  }
  
  /* 详细布局 (≥ 350px) */
  @container product (min-width: 350px) {
    display: flex;
    gap: 1rem;
    
    .card-content {
      flex: 1;
      
      .description {
        display: block;
        margin: 0.5rem 0;
      }
    }
  }
  
  /* :has() 魔法 - 视频标记 */
  &:has(.media-video)::before {
    content: "🎬";
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
    background: rgba(0,0,0,0.7);
    padding: 0.25rem 0.5rem;
    border-radius: 0.25rem;
  }
  
  /* 折扣标记 */
  &:has(.badge-discount) .price {
    color: #dc2626;
    
    &::after {
      content: " 🔥";
    }
  }
  
  /* 缺货暗淡效果 */
  &:has(.badge-out-of-stock) {
    opacity: 0.5;
    filter: grayscale(0.3);
  }
}

📊 浏览器兼容性 (2025)

特性ChromeFirefoxSafari
Container Queries105+110+16+
:has()105+121+15.4+
CSS Nesting112+117+16.5+
@scope118+不支持17.4+

明日预告:Web Components 4.0 - 跨框架组件开发的终极方案(包括声明式 Shadow DOM、CSS 作用域增强)

💡 今日金句:现代CSS让组件真正拥有了"自我意识",不再被动依赖全局视口!