选择器与盒模型

上一章建立了 CSS 的最小认知,本章补齐两块最硬的骨头:选择器大全与优先级记分法(决定样式写在哪)、盒模型(决定每个元素占多大地方)。前端面试的常客、日常调试的高频案发现场,都在这一章。


选择器大全

属性选择器

按属性的存在与否或取值匹配:

/* 存在即命中 */
[disabled] { opacity: 0.5; }
 
/* 完全等于 */
[type="text"] { border: 1px solid #ccc; }
 
/* 开头匹配:外链图标惯例 */
a[href^="https://"] { padding-right: 14px; }
 
/* 结尾匹配:下载链接加图标 */
a[href$=".pdf"]::after { content: " (PDF)"; }
 
/* 包含匹配 */
[class~="btn"] { cursor: pointer; }   /* ~ 是按词匹配,等价 .btn 但更慢 */

实用场景:给表单控件统一风格(input[type="text"])、标记外部链接、文件类型图标。

伪类:状态选择器

伪类以单个冒号开头,匹配元素的状态或位置

/* 交互状态四件套:注意 LVHA 书写顺序 */
a:link    { color: blue; }   /* 未访问 */
a:visited { color: purple; } /* 已访问 */
a:hover   { color: red; }    /* 悬停 */
a:active  { color: orange; } /* 按下瞬间 */
 
/* 表单状态 */
input:focus        { border-color: #2563eb; outline: none; }
input:checked      { accent-color: #2563eb; }
input:disabled     { background: #f3f4f6; }
input:placeholder-shown { border-style: dashed; }
 
/* 结构位置类 */
li:first-child  { font-weight: bold; }  /* 第一个孩子 */
li:last-child   { border-bottom: none; }
tr:nth-child(odd)   { background: #fafafa; } /* 奇数行斑马纹 */
tr:nth-child(3n)    { background: #eef; }    /* 每 3 个一次 */
li:nth-last-child(2) { color: gray; }        /* 倒数第二个 */
 
/* 否定伪类:排除 */
li:not(:last-child)::after {
  content: ",";   /* 列表项之间加逗号,最后一项不加 */
}

nth-child 的公式语法 an+b 很灵活:2n 偶数、2n+1 奇数、n+3 从第 3 个开始全中。注意它数的是所有兄弟元素不看标签类型,混排容器里容易翻车——更精确的是 nth-of-type 系列。

伪元素:虚拟子元素

双冒号开头(老单冒号写法兼容),创造不存在的元素用于装饰:

/* ::before/::after:在内容前后插入生成内容,必须有 content */
.quote::before { content: "\201C"; font-size: 3em; color: #ddd; }
 
/* 清除浮动的祖传写法(读懂老项目用) */
.clearfix::after {
  content: "";
  display: block;
  clear: both;
}
 
/* ::selection:用户选中文字时的颜色 */
p::selection { background: #2563eb; color: white; }
 
/* ::placeholder:占位符文字样式 */
input::placeholder { color: #9ca3af; }

before/after 是 CSS 装饰艺术的核心武器:引号、分隔点、三角箭头、遮罩层都靠它们凭空造出来,HTML 保持干净。

组合器:关系选择器

/* 后代选择器(空格):所有深度的子孙 */
article p { line-height: 1.8; }
 
/* 直接子代 >:只有儿子辈 */
nav > ul > li { display: inline-block; }
 
/* 相邻兄弟 +:紧跟着的下一个弟弟 */
h2 + p { margin-top: 0; }          /* 标题后紧跟的第一段不留上边距 */
 
/* 通用兄弟 ~:后面的所有弟弟(同一父元素内) */
h2 ~ p { color: #374151; }

组合器让”上下文样式”变得精准:比如”表格里 hover 到某行时高亮它的第一列”:

tr:hover td:first-child { background: #eff6ff; }

优先级计算规则详解

上一章给了直觉版,现在是精确算法。浏览器把每条选择器折算成一个三元组分数 (A, B, C)

统计对象记分
Aid 选择器每个 +1
B类 / 伪类 / 属性选择器各 +1
C元素 / 伪元素选择器各 +1

通配符 *、组合器(> + ~ 空格)不计分。比较规则:从 A 位开始逐位比,高位大者胜;全平则源码顺序后者胜。

*               {}  /* (0,0,0) */
p               {}  /* (0,0,1) */
p::before       {}  /* (0,0,2) 伪元素算 C 位 */
.warning        {}  /* (0,1,0) */
p.warning       {}  /* (0,1,1) */
#header p       {}  /* (1,0,1) */
.nav li.active  {}  /* (0,2,1) 两个类一个元素 */

实战推演——谁赢?

<nav id="main">
  <ul><li class="active">首页</li></ul>
</nav>
li                {}  /* (0,0,1) */
.active           {}  /* (0,1,0) */
nav li            {}  /* (0,0,2) */
nav .active       {}  /* (0,1,1) */
#main .active     {}  /* (1,1,0) <- 冠军 */

三个铁律:

  1. A 位碾压一切 B/C 位:(1,0,0) 大于无数个类的 (0,99,99)。这就是”id 少用于样式”的根本原因——一个 id 上场,后面所有想覆盖它的规则都得拉出更大的 id,优先级通胀从此开始
  2. 行内 style 相当于 (1,0,0,0) 更高一位,只能靠 !important 对抗
  3. !important 超脱整个体系,多条 important 之间再按正常记分法比

类比后端:优先级像权限系统的角色继承——具体角色的授权覆盖通用角色的默认值,而 !important 是 root 权限,滥用等于全员 sudo。


盒模型完整讲解

页面上每个元素都是一个盒子,由四层同心结构组成:

graph TD
    A["margin 外边距<br>与其他元素保持的距离(透明)"] --> B["border 边框"]
    B --> C["padding 内边距<br>边框与内容的缓冲带(随背景)"]
    C --> D["content 内容区<br>文字图片所在"]
.box {
  width: 200px;
  padding: 20px;
  border: 5px solid #333;
  margin: 30px;
}

关键问题:这个盒子实际多宽?答案取决于 box-sizing。

box-sizing: border-box 为什么必设

两种计算模式:

模式width 指的是实际占用宽度
content-box(默认)仅内容区width + padding×2 + border×2 = 200+40+10 = 250px
border-box内容+内边距+边框整体width 就是 200px,改 padding 会压缩内容区

content-box 的问题:你声明 width: 25% 再随手加个 padding: 10px,实际宽度就超过 25% 了——布局像纸牌屋一样塌掉。border-box 让”你看到的 width 就是最终宽度”,心智负担骤降。

所以业界共识是全局设置:

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

几乎所有现代 CSS 框架(Tailwind、Bootstrap)的第一行都是它。上一章实战代码里你已经见过这针疫苗。

margin 合并问题

垂直方向相邻的两个块级元素,它们的 margin 会合并取较大者而非相加:

.p1 { margin-bottom: 30px; }
.p2 { margin-top: 20px; }
/* 两段之间的实际间距是 30px,不是 50px */

三种触发场景:

graph LR
    A["margin 合并场景"] --> B["相邻兄弟的上下边距合并"]
    A --> C["父元素与第一个/最后一个子元素合并<br>子元素的 margin 顶穿了父元素"]
    A --> D["空块元素自己的上下面距合并"]

“父元素与子元素合并”是最坑的场景:给父元素里第一个子元素设 margin-top,结果父元素整体被顶下去了,父元素自己没获得任何间隔。阻断手段任选其一:父元素设 padding 或 border 隔开、设 overflow: hidden、改用 Flex/Grid 容器(弹性/网格容器内部不发生 margin 合并)。

注意:水平方向永不合并,左右 margin 是老实相加的。

display 常用值

display 决定元素的排布身份:

取值行为典型代表
block独占一行,可设宽高div/p/h1/section
inline行内流动,宽高无效,上下 margin 无效span/a/strong
inline-block不换行 + 可设宽高,两者兼得自定义按钮、导航项
none直接从渲染树消失,不占位配合 JS 显示隐藏
flex / grid变身弹性/网格容器见后续三章

inline 元素的经典困惑:给 span 设 width 没反应——因为它根本不是盒子模型意义上的”块”,宽高被无视。要能设尺寸又不换行,用 inline-block。

三者对比速记:

<div>block:我独占一行,能设宽高</div>
<span>inline:我跟着文字流,设了宽高也没人理</span>
<button>inline-block:我不换行但可以调大小</button>

实战:卡片组件样式

做一个通用卡片组件,综合运用本章全部知识:属性选择器、伪类、伪元素、优先级控制、盒模型、margin 处理。保存为 card.html 打开。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>卡片组件 · 选择器与盒模型实战</title>
  <style>
    /* ===== 全局重置 ===== */
    *, *::before, *::after { box-sizing: border-box; }
    body {
      font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
      background: #f3f4f6;
      padding: 40px 16px;
    }
 
    /* ===== 卡片主体 ===== */
    .card {
      background: #fff;
      border-radius: 12px;
      padding: 24px;              /* 内边距撑起呼吸感 */
      max-width: 360px;
      margin-bottom: 24px;        /* 卡片间距交给下边距统一管理 */
      box-shadow: 0 1px 3px rgba(0,0,0,.1);
      transition: box-shadow .2s, transform .2s;
    }
    /* 悬停抬升反馈:transform 与阴影过渡,动画细节见 CSS 动画章 */
    .card:hover {
      transform: translateY(-4px);
      box-shadow: 0 8px 24px rgba(0,0,0,.15);
    }
 
    /* ===== 卡片头 ===== */
    .card__title {
      font-size: 18px;
      margin-bottom: 8px;
    }
    /* 用伪元素造标题下的装饰短线:不污染 HTML */
    .card__title::after {
      content: "";
      display: block;
      width: 40px;
      height: 3px;
      background: #2563eb;
      margin-top: 8px;
      border-radius: 2px;
    }
 
    /* ===== 徽标:属性选择器按变体着色 ===== */
    .badge {
      display: inline-block;      /* 行内但要能设 padding 和圆角 */
      font-size: 12px;
      padding: 2px 10px;
      border-radius: 999px;
      background: #e5e7eb;
      color: #374151;
    }
    .badge[data-level="hot"]  { background: #fee2e2; color: #b91c1c; }
    .badge[data-level="new"]  { background: #dbeafe; color: #1d4ed8; }
 
    /* ===== 底部操作区 ===== */
    .card__footer {
      display: flex;              /* 先借用 flex 排两端,详见下一章 */
      justify-content: space-between;
      align-items: center;
      margin-top: 16px;
      padding-top: 16px;
      border-top: 1px solid #f3f4f6;  /* 分隔线用 border 不用 hr */
    }
    .card__price {
      font-weight: bold;
      color: #dc2626;
    }
    .card__btn {
      border: none;
      background: #2563eb;
      color: #fff;
      padding: 8px 20px;
      border-radius: 8px;
      cursor: pointer;
    }
    .card__btn:focus-visible {    /* 键盘聚焦时才显示轮廓,鼠标点击不显示 */
      outline: 2px solid #2563eb;
      outline-offset: 2px;
    }
    .card__btn:disabled {
      background: #d1d5db;
      cursor: not-allowed;
    }
 
    /* ===== 斑马纹列表:结构伪类实战 ===== */
    .card ul { list-style: none; }
    .card li {
      padding: 6px 0;
    }
    .card li:nth-child(odd) {
      background: #f9fafb;
    }
    /* 最后一项去掉多余间距:not 否定伪类 */
    .card li:not(:last-child) {
      border-bottom: 1px dashed #e5e7eb;
    }
  </style>
</head>
<body>
 
  <!-- 卡片一:热销 -->
  <div class="card">
    <span class="badge" data-level="hot">热销</span>
    <h3 class="card__title">CSS 布局实战营</h3>
    <ul>
      <li>Flexbox 六大容器属性精讲</li>
      <li>Grid 十二列栅格演练</li>
      <li>响应式断点设计方法</li>
    </ul>
    <div class="card__footer">
      <span class="card__price">¥199</span>
      <button type="button" class="card__btn">立即报名</button>
    </div>
  </div>
 
  <!-- 卡片二:新课 + 禁用按钮演示 -->
  <div class="card">
    <span class="badge" data-level="new">新课上架</span>
    <h3 class="card__title">TypeScript 入门到工程化</h3>
    <ul>
      <li>类型系统核心概念</li>
      <li>泛型与类型体操入门</li>
    </ul>
    <div class="card__footer">
      <span class="card__price">¥149</span>
      <!-- disabled 态:观察 :disabled 与 :focus-visible 的效果差异 -->
      <button type="button" class="card__btn" disabled>已满员</button>
    </div>
  </div>
 
</body>
</html>

知识点对照表:

代码位置用到的本章知识
box-sizing: border-box 全局重置盒模型统一度量
.badge[data-level="hot"]属性选择器做组件变体
.card__title::after 画短线伪元素免 HTML 装饰
li:nth-child(odd) / li:not(:last-child)结构伪类与否定伪类
.card__btn:disabled / :focus-visible状态伪类
badge 的 display: inline-block行内但需要尺寸的场景
footer 的 border 分隔线替代 hr盒模型边框的活用

命名里的 card__title 双下划线是 BEM 命名法(块__元素)的痕迹——它通过命名纪律规避优先级战争,大型项目值得了解。

下一步解决真正的布局问题:Flexbox 布局