CSS入门

前言

  • CSS(Cascading Style Sheets,层叠样式表)负责网页的呈现:颜色、字号、间距、位置、响应式行为。HTML 说「这是什么」,CSS 说「它长什么样」。
  • 本篇的所有数字都是实测出来的:本机 Chrome,视口 888 × 858,devicePixelRatio = 1.25,根字号 16px。换浏览器、换系统字体、换窗口大小都会变,所以重点看规律而不是绝对值。

一句话说清「层叠」

CSS 的一条最终样式,是四件事叠加的结果:来源 → 层(layer)→ 优先级(specificity)→ 书写顺序。

  • 来源:浏览器默认样式 < 用户样式表 < 作者样式表(我们写的)。
  • 层:@layer 显式分层后,未分层的样式永远赢过分层的样式,与权重无关。实测:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @layer base {
    #layer1 { color: rgb(101, 101, 101); } /* 分层里的 id */
    }
    #layer1 { color: rgb(102, 102, 102); } /* 未分层的 id -> 实测生效 */

    @layer base {
    #layer2 { color: rgb(103, 103, 103); } /* 分层里的 id */
    }
    .plain { color: rgb(104, 104, 104); } /* 未分层的 class -> 实测生效 */
    两条都读了未分层的那条颜色,说明 id 权重在分层面前不起作用。
  • 优先级:见下一节。
  • 书写顺序:前三步都平手时,写在后面的赢。

继承(inherit)不参与优先级竞争:只要有任何一条规则命中元素,就轮不到继承值。实测 .parent { color: A } 里的 <p class="k8"> 被 p.k8 { color: B }(权重仅 0,1,1)覆盖。

语法构成

1
2
3
4
5
6
/* 注释:由一对「斜杠+星号」包住;注释里不能再出现结束符,否则会提前截断 */
选择器1,
选择器2 { /* 声明块:花括号包起来 */
属性: 值; /* 一条声明 = 属性 + 冒号 + 值,以分号结尾 */
属性2: 值 单位;
} /* 最后一个分号可以省,但别省 */
  • 选择器与 { 之间、: 两侧的空格都可有可无;属性名和值之间必须有冒号,单位紧跟数字不能有空格(margin: 10 px 无效,margin: 10px 才对)。
  • 大小写:标签名、属性名、关键字不区分大小写(COLOR: RED 合法),但类名/ID 区分(.Box 与 .box 不同)。约定全部小写最省心。
  • 属性名不能拼错也不能用驼峰:background-color 不是 backgroundColor(只有写在 JS 里才驼峰)。

三种引入方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<html lang="zh-cn">

<head>
<meta charset="utf-8">
<title>CSS 引入方式</title>

<!-- 方式二:内部样式表,只作用于当前页面 -->
<style>
p {
color: white; /* p 的文字颜色 */
}
</style>

<!-- 方式三:外部样式表,可被整站复用,type="text/css" 已不需要 -->
<link rel="stylesheet" href="05-css.css">
</head>

<!-- 方式一:行内样式(写在标签的 style 属性里),优先级最高、最难维护 -->
<body style="background-color: orange;">
<p>曾经沧海难为水,除却巫山不是云</p>
<div>床前明月光,疑是地上霜</div>
</body>

</html>
1
2
3
4
/* 05-css.css */
div {
color: blue; /* 外联样式:div 的文字颜色 */
}
方式 写在哪 复用范围 什么时候用
行内 style 标签属性 单个元素 只有一处、且值是 JS 动态算出来的(如进度条宽度)
内部 <style> 当前页 head 单个页面 单页工具、邮件模板、示例代码
外部 <link> .css 文件 整个站点 默认选择:可缓存、可复用、职责清晰
  • 行内样式在层叠里权重最高(1,0,0,0),这也是「为什么我 CSS 写了没效果」的头号原因。
  • <link> 建议放 <head>,避免先渲染无样式内容(FOUC)。

选择器与优先级

优先级怎么算:四元组 (a, b, c, d)

位置 计数内容 例子
a 行内样式(写在 style="" 里) 1,否则 0
b ID 选择器个数 #nav → 0,1,0,0
c 类、属性、伪类个数 .btn、[type="text"]、:hover → 各算一个
d 元素选择器、伪元素个数 p 算一个,::before 算一个
— 通配 *、组合器(> + ~ 空格)、:where()、:is()/:where()/:not()/:has() 内部的贡献为 0 :is(#a, .b) 整体按最高的那个算(即 #a 的 1,0,0,0)

比大小从高位到低位逐位比较,不跨位进位:0,99,0,0 也赢不过 1,0,0,0。

实测结论(同一页面里,读最终 color):

对局 结果
#r1(1,0,0) vs .k1a.k1b.k1c.k1d(0,4,0) id 赢
.k2(0,1,0) vs 后写的 [data-k2](0,1,0) 后写的赢(等权重看顺序)
同一条 .k3 写两遍 后一遍赢
样式表里 .k4 { …!important } vs 行内普通样式 !important 赢
行内 !important vs #case5 …!important 行内赢(行内是 1,0,0,0,压过 id 的 0,1,0,0)
.k6(0,1,0) vs :is(#not-exist-id, .k6)(1,0,0) :is 赢,即使那个 id 根本不存在
.k7(0,1,0) vs :where(#not-exist-id, .k7)(0,0,0) .k7 赢,:where 不加分
p.k8(0,1,1) vs 从父元素继承的颜色 规则赢,继承不参与竞争

结论性建议

  • 别用 !important 解决优先级问题;它是逃生舱,用来覆盖第三方库。用多了只能靠更 !important 往上叠。
  • a 位(行内)几乎打不过,除非你也用 !important。所以动态样式尽量用 CSS 变量或类名切换,而不是 el.style.xxx。
  • 类名扁平一点:.card .body .title span 这种长链权重高、难覆盖、还容易因 DOM 变化失效。项目里更常见的是给元素本身一个类(.card-title)。
  • 想让选择器不增加权重,套一层 :where()::where(:not(:last-child))、:is() 用来合并写法。

基础与复合选择器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* 标签(类型)选择器 */
p { color: blue; }

/* 类选择器:可以一个元素挂多个类 */
.highlight { background-color: yellow; }

/* ID 选择器:一个页面内 id 必须唯一 */
#header { font-size: 24px; }

/* 交集(同时满足):注意中间没有空格 */
div.banner.hot { border: 1px solid red; }

/* 后代:空格,命中所有层级的子元素 */
article p { line-height: 1.8; }

/* 子代:> 只命中直接子元素 */
ul > li { list-style-type: none; }

/* 相邻兄弟 +:紧贴在后面的那一个 */
h2 + p { margin-top: 0; }

/* 通用兄弟 ~:后面的所有兄弟 */
h2 ~ p { color: #444; }

/* 并集(分组):逗号,等价于把规则抄几份 */
h1, h2, h3 { font-weight: 700; }

/* 通配:常见于样式重置;性能影响在现代浏览器里可忽略 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
  • div p 与 div > p 的区别是「后代」与「子代」,这是新手最容易踩的一条。
  • .a .b(空格)≠ .a.b(交集):前者要求 b 是 a 的后代,后者要求同一元素同时有两个类。

属性选择器

1
2
3
4
5
6
7
input[required] { border-color: red; }             /* 有该属性 */
input[type="text"] { background-color: lightgray; } /* 值精确相等 */
a[href^="https://"] { color: green; } /* 前缀匹配 */
a[href$=".pdf"] { padding-right: 18px; } /* 后缀匹配 */
img[src*="thumb"] { border: 1px solid #ccc; } /* 包含 */
[class|="btn"] { font-weight: bold; } /* 值正好是 btn 或以 btn- 开头 */
input[type="search" i] { width: 100%; } /* 加 i 忽略大小写(值匹配) */
  • [attr] 匹配的是写在 HTML 里的属性,不是 JS 改过的属性值——用 el.href = x 不会让 [href="x"] 命中(要改必须 setAttribute)。
  • 属性选择器权重与类相同(0,1,0)。

伪类(状态)

1
2
3
4
5
6
7
8
9
10
11
12
a:link { color: blue; }              /* 未访问 */
a:visited { color: purple; } /* 已访问 */
a:hover { color: red; } /* 悬停 */
a:active { color: orange; } /* 激活(按下中) */
a:focus-visible { outline: 2px solid #06c; } /* 键盘聚焦才显示轮廓 */

input[type="checkbox"]:checked + span { font-weight: 700; } /* 选中态 */
input:focus { border-color: #06c; }
input:disabled { background: #f5f5f5; color: #999; }
input:invalid { border-color: #d33; }
li:first-child, li:last-child, li:nth-child(2n) { /* 首、尾、偶数项 */ }
p:not(.note) { color: #333; } /* 排除 */
  • 链接四态的顺序建议 LoVe FAIR(:link → :visited → :hover → :active),否则后写的会被前面的盖掉。
  • :visited 出于隐私限制,浏览器只允许改少量颜色相关属性,且 JS 读不到访问状态;实测给 a:visited 写 background-color 时,计算值仍是透明。
  • :focus-visible 比 :focus 好用:鼠标点击不显示轮廓,键盘 Tab 才显示。
  • :nth-child(An+B) 里 2n 即偶数、2n+1 奇数、3n 每第三个;:nth-of-type 按同类型元素计数。

伪元素

1
2
3
4
5
6
p::before { content: "→ "; color: red; }           /* 内容之前,必须写 content 才生效 */
button::after { content: " Click me!"; font-weight: 700; }
p::first-letter { font-size: 2em; color: blue; } /* 首字母 */
p::first-line { font-weight: bold; } /* 首行 */
::selection { background: yellow; color: black; } /* 选中文本 */
li::marker { color: rgb(77, 88, 99); } /* 列表标记本身(实测可上色) */
  • :: 是两个冒号,: 是伪类。历史原因允许 :before 这种单冒号写法,但统一用双冒号更清楚。
  • 伪元素不是真实节点:JS 取不到它、也点不到它;content: "" 时它依然存在(这就是画三角形、画小圆点的基础)。
  • 实测 li::marker { color } 能改项目符号颜色,list-style-type 也能在 ::marker 里重写。

现代选择器补充(实测本机 Chrome 全支持)

1
2
3
4
article:has(> img) { border: 1px solid red; }        /* 父选择器:含直接子 img 的 article */
:is(h1, h2, h3) .title { color: red; } /* 分组,权重取参数里最高的 */
:where(h1, h2, h3) .title { color: red; } /* 分组,权重 0,方便覆盖 */
input:-webkit-autofill { box-shadow: 0 0 0 1000px #fff inset; } /* 浏览器自动填充的配色干预 */
  • :has() 是 CSS 里长期缺失的「向上选」,CSS.supports('selector(:has(*))') 实测 true。
  • :is() 写起来省字,但会继承参数里最高的权重,覆盖时容易翻车;不确定就用 :where()。

长度单位与数值

实测换算(视口 888×858,dpr 1.25,根字号 16px)

单位 含义 实测
px CSS 逻辑像素(不是物理像素) 基准,1in = 96px
1pt 排版点,1/72 英寸 1.33333px
1pc 排版派卡,12pt 16px
1in 英寸 96px
1cm 厘米 37.7953px
1mm 毫米 3.77953px
1em 当前元素自己的 font-size 倍数 父 2rem 时,子 0.875em = 28px
1rem 根元素(<html>)font-size 倍数 2rem = 32px
1%(字号) 相对父元素字号 150% = 24px
1ch 字体「0」的 advance 宽度 10ch = 88.8px
1ex 字体 x-height 2ex = 17.376px
1vw 视口宽度的 1% 8.88px(888/100)
1vh 视口高度的 1% 8.576px(858/100)
50dvh 动态视口高度(含/不含浏览器工具栏都会跟着变) 428.8px

用哪一族的经验

  • 字号:rem 给全局尺度(改一处根字号,整站跟着缩放,对用户也更友好),组件内部用 em 让它跟着自身字号走。
  • 间距:px/rem;% 用来做与父宽挂钩的间距。
  • 大块的「铺满/占几成」:%、vw/vh,视口高度在手机上有历史包袱,尽量用 dvh(实测支持)。

百分比 padding/margin 的坑(实测)

1
2
3
4
5
.card {
width: 200px; /* 自己宽度 200px */
padding: 10%; /* 但 padding 按「包含块宽度」算 */
font-size: 10px;
}

父容器内容宽度为 872.75px 时,实测 padding-left = 87.275px。也就是说:

  • 上下 padding / margin 的百分比也按「宽度」算,这是做「16:9 占位块」(padding-top: 56.25%)的原理。
  • 只有 font-size、line-height 的百分比才是按字号来的。

calc() 与函数族(实测)

1
2
3
4
5
6
7
.bar {
width: calc(100px + 2rem); /* 实测 132px */
max-width: min(80%, 250px); /* 实测父宽 200px 时取 160px */
width: clamp(100px, 50%, 300px); /* 实测取 100px(50% 算出来正好等于下限) */
height: 50dvh; /* 实测 428.8px */
line-height: 1.5lh; /* lh 单位:实测支持 */
}
  • calc() 里 + - * / 两侧必须有空格(calc(100%-10px) 无效)。
  • min() / max() / clamp() 是「响应式不需要媒体查询」的主力:font-size: clamp(1rem, 2.5vw, 1.5rem) 一行搞定随屏放大又不失控。
  • 关键字类取值:auto、inherit、initial、unset、revert;none、normal、block 等按属性而定。min-width / max-width 是属性名,不是单位也不是关键字。

数字与颜色字面量

1
2
3
4
5
6
7
8
9
10
11
p {
color: rgb(51, 51, 51); /* 函数式 */
color: rgba(51, 51, 51, 0.5); /* 带 alpha */
color: #333333; /* 十六进制 */
color: #333; /* 三位可缩写 */
color: #33333380; /* 八位,末两位是 alpha */
color: hsl(210deg 50% 40%); /* 色相/饱和/亮度 */
color: hsl(210 50% 40% / 0.5); /* 带 alpha 的新语法 */
color: rebeccapurple; /* 148 个命名色之一 */
color: color-mix(in oklab, red 30%, blue); /* 实测计算值:oklab(0.504777 0.0447402 -0.180308) */
}
  • color-mix() 实测在 Chrome 里返回的是 oklab(...) 而不是 rgb——它是「按感知均匀的色彩空间混合」,比老式 rgba 叠加更可控(CSS.supports('color: color-mix(in oklab, red, blue)') = true)。
  • 透明度优先用带 alpha 的颜色,而不是 opacity: ——后者会让整个元素(含子元素、含文字)都变透明,并且会创建层叠上下文。

字体与文本

字体族:font-family 是一条「降级链」

1
2
3
4
5
6
7
8
9
10
body {
/* 从左到右找,找到第一个可用的就用;带空格或中文的字体名必须加引号 */
font-family: "Helvetica Neue", Helvetica, Arial, "PingFang SC",
"Microsoft YaHei", "Noto Sans SC", sans-serif;
font-size: 1rem;
line-height: 1.6;
}
code, pre, kbd {
font-family: Consolas, "Courier New", monospace; /* 等宽链收尾用 monospace */
}
  • 结尾一定要留一个通用族(serif、sans-serif、monospace、cursive、system-ui、math、emoji…)。
  • 实测本机 <body> 的默认字体族被解析成 "Noto Sans SC"——默认值取决于系统,所以「我这看着好,你那看着歪」是正常的,别依赖默认字体。
  • 中文字体名一定要引号包裹;PingFang SC(苹果)、Microsoft YaHei(Windows)、Noto Sans SC/Source Han Sans(跨平台)是常见组合。

字号、粗细、倾斜

属性 常用值 实测/注意
font-size 16px、1rem、clamp(...) 浏览器最小字号限制会让 font-size: 8px 看起来是 12px(中文环境常见)
font-weight 100~900、normal(400)、bold(700) 只有该字重真的存在才有效果;b/strong 默认 700 实测与 CSS 写 700 一致
font-style normal、italic、oblique 字体没做真斜体时浏览器会倾斜拉伸(伪斜体),中文尤其明显
font-variant small-caps、tabular-nums 数字用等宽 font-variant-numeric: tabular-nums 对齐表格
font-feature-settings "liga" 0 精细控制 OpenType 特性,日常用 font-variant-* 更可读

line-height:三种写法的差别(实测)

1
2
3
4
.a { font-size: 16px; line-height: normal; }   /* 实测计算值仍是 "normal",不会给数字 */
.b { font-size: 16px; line-height: 1.6; } /* 实测 25.6px = 16 × 1.6 */
.c { font-size: 16px; line-height: 16px; } /* 固定值 */
.d { font-size: 16px; line-height: 150%; } /* 百分比:按自身字号算,但会被继承成绝对值 */
  • 推荐写无单位倍数(1.6):它传给子元素的是「比例」,子元素按自己的字号重新计算;写 1.6em 或 150% 时子元素继承的是已算出的长度,嵌套越深越容易出错。
  • 想让单行文字垂直居中:line-height 等于容器高度即可(多行不行)。

文本修饰与排版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
p {
color: #333;
text-align: justify; /* left / right / center / justify */
text-indent: 2em; /* 首行缩进,中文排版标配 */
text-decoration: underline; /* 简写:line + style + thickness + color */
text-transform: capitalize; /* uppercase / lowercase / capitalize / full-width */
letter-spacing: 2px; /* 字距 */
word-spacing: 5px; /* 词距(中文几乎没有「词」,效果不明显) */
white-space: normal; /* normal / nowrap / pre / pre-wrap / pre-line */
overflow-wrap: break-word; /* 实测计算值 "break-word",长单词才需要 */
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); /* 实测计算值把颜色写在最前面 */
direction: ltr; /* 文字方向;配合 unicode-bidi 处理混合方向 */
writing-mode: horizontal-tb; /* vertical-rl 可做中文竖排 */
}
  • text-align 只管内联内容在这个块里的对齐;要让块本身居中得靠 margin: 0 auto、flex 或 grid。
  • text-transform 只改渲染,不改 DOM:实测 text-transform: capitalize 之后 el.textContent 仍是 hello world from css。读屏软件、复制粘贴拿到的都是原文。
  • 中文两端对齐 justify 一般需要 text-align-last: justify(控制最后一行)配合。

单行省略号三件套(实测)

1
2
3
4
5
.ellipsis {
overflow: hidden; /* 1. 溢出必须裁掉 */
white-space: nowrap; /* 2. 必须不换行 */
text-overflow: ellipsis; /* 3. 用省略号表示截断 */
}

实测:满足三条件时 scrollWidth 496 > clientWidth 120,出现 …;只写 text-overflow: ellipsis 而不加 nowrap(多行场景)不会生效(实测内容只是换行溢出,scrollWidth 124 / clientWidth 120)。

多行截断要用 -webkit-line-clamp(现在基本所有浏览器都支持):

1
2
3
4
5
6
.clamp3 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3; /* 最多 3 行,多余部分用省略号 */
overflow: hidden;
}

vertical-align 到底在管什么

  • 它只对行内元素(inline / inline-block)和表格单元格生效,给块级元素写它没有任何反应——「用 vertical-align: middle 让 div 居中」是无效操作。
  • 语义是相对当前行的基线定位,不是「相对父元素居中」。实测(#va 容器 line-height: 40px,字体 16px,量的是各 span 顶边相对容器顶边的偏移):
    写法 实测顶边偏移
    baseline +18px
    top +8px
    middle +20px
    bottom +20px
    10px +8px(在基线上再上移 10px:18 − 10 = 8)
  • 最后一行说明:vertical-align: <长度> 是相对基线的位移,正数向上。
  • 真要「垂直居中」,用 flex(align-items: center)或 grid(place-items: center)。

颜色与背景

背景属性全家福

1
2
3
4
5
6
7
8
9
10
.box {
background-color: #ff0000; /* 背景色 */
background-image: url("bg.png"); /* 背景图(可以是多层,逗号分隔) */
background-repeat: no-repeat; /* repeat / repeat-x / repeat-y / no-repeat / space / round */
background-position: top right; /* 位置 */
background-size: cover; /* cover / contain / 长度 / 百分比 */
background-attachment: scroll; /* scroll / fixed / local */
background-origin: content-box; /* 定位的基准盒:padding-box / content-box / border-box */
background-clip: padding-box; /* 背景的绘制范围,含 border-box / content-box / text */
}

简写顺序是「color image repeat position / size origin clip」,实测:

1
.short { background: #ff0000 url("bg.png") no-repeat top right / cover; }
计算值 实测结果 说明
background-color rgb(255, 0, 0) 简写里可以不带颜色
background-repeat no-repeat —
background-position 100% 0% 关键字 top right 被换算成百分比
background-size cover 斜杠后面就是 size
background-attachment scroll 默认值

多背景(先写的在上层):

1
2
3
4
5
.hero {
background-image: url("pattern.png"), linear-gradient(#ffffff, #000000);
background-repeat: no-repeat, repeat; /* 实测计算值就是 "no-repeat, repeat" */
background-size: cover, auto;
}
  • 渐变本身就是 background-image 的一种:linear-gradient(45deg, red, blue)、radial-gradient(circle at 30% 70%, …)、conic-gradient(…)(做饼图/角度色环很省事)。
  • background-clip: text + -webkit-text-fill-color: transparent 可以做「渐变文字」。
  • 半透明遮罩的标准做法:把 linear-gradient(rgba(0,0,0,.4), rgba(0,0,0,.4)) 作为第一层背景,图片作为第二层,不用额外加一个 div。

background-attachment 三个值的真实差别

1
2
3
scroll  背景相对元素固定:元素随页面滚走,背景跟着走,但不会在元素内部偏移(默认)
fixed 背景相对视口固定:滚动页面时背景纹丝不动(常用于全屏大图;移动端浏览器支持不佳)
local 背景随元素内容滚动:元素自身出现滚动条时,背景跟着内容一起移动

原笔记把 scroll 解释成「随内容滚动」,正好和 local 说反了。

前景色与边框

1
2
3
4
5
6
7
8
9
10
11
.card {
color: #1a1a1a; /* 文字颜色,会被继承 */
border: 1px solid #ddd; /* 宽度 样式 颜色,三者顺序任意 */
border-radius: 8px; /* 圆角;可写 4 个值分别控制 */
border-top-left-radius: 0; /* 单角 */
outline: 2px solid #06c; /* 轮廓:不占空间、不参与布局 */
outline-offset: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, .12); /* 阴影:x y blur spread color */
/* 想要「内阴影」:box-shadow: inset 0 0 0 1px #eee; */
filter: drop-shadow(0 2px 4px rgba(0,0,0,.3)); /* 沿不规则形状的真实轮廓投影 */
}
  • border 有宽度就一定占空间(盒模型里算进去),outline 不占——所以聚焦态用 outline 不会引起抖动。
  • 圆角配合 border-radius: 50% 做圆形头像,前提是先固定宽高。
  • 想要「1 物理像素」的细线(Retina 屏上 1px 看着偏粗),常见做法是画一条 transform: scaleY(.5) 的线。

盒子模型

每个元素都是一个矩形盒子,从内到外:content → padding → border → margin。

CSS 盒模型:由内到外 content → padding → border → margin,每一层都在上一层外面

实测:box-sizing 改变的是「width 指什么」

1
2
3
.box { width: 300px; padding: 20px; border: 5px solid black; margin: 30px; }
.content-box { box-sizing: content-box; } /* 默认(标准盒模型) */
.border-box { box-sizing: border-box; } /* 常被称为「IE 盒模型」 */
量法 content-box border-box 解释
style.width 声明值 300px 300px 都是 300px,但含义不同
getBoundingClientRect().width 350 300 350 = 300 + 20×2 + 5×2
offsetWidth 350 300 border 外沿
clientWidth 340 290 border 内沿(= 内容 + padding)
  • 标准盒模型:width 只是内容宽,实际占位要再加 padding 和 border。
  • border-box:width 就是 border 外沿的总宽,padding/border 从里面扣(内容宽变成 250px)。
  • 所以现在几乎每个项目都先写一句全局重置:
1
2
3
*, *::before, *::after {
box-sizing: border-box; /* 让「写多少就是多少」,算宽度不再抓狂 */
}

示意图(原笔记留下的两张图仍然适用):

标准盒模型

IE 盒模型

margin:会合并、会穿透(全部实测)

1
2
.a { margin: 20px; height: 30px; }
.b { margin: 30px; height: 30px; }
  • 相邻块级元素的上下 margin 合并:实测 .a 下边 20px 与 .b 上边 30px 之间的实际间距是 30px,不是 50px(取较大者;有负值时按 最大值 + 最小值 计算)。
  • 父子穿透:父元素没有 border-top / padding-top 时,子元素的 margin-top 会「冒」到父元素外面。实测子元素 margin-top: 40px:
    • 普通父容器:子元素顶边相对父容器顶边偏移 0px(margin 穿透,把整块往下推)
    • 父容器加 padding-top: 1px:偏移 41px(穿透被打断)
    • 父容器改 display: flow-root:偏移 40px(干净的阻断方式,不需要真的加内边距)
  • 空盒子自己合并:上下 margin 直接穿过去合并,中间不留高度(实测高度 0)。
  • 阻断 margin 合并的常用手段:display: flow-root(现代首选)、padding/border、overflow: hidden(副作用是裁掉溢出内容)、变成 flex/grid 容器(其子项不合并)。
  • 行内元素上下 margin 无效:实测给 <span> 写 margin: 30px; width: 300px; height: 100px; 后,它的盒子只有 287 × 43——width/height 被忽略,高度由行高决定;左右 margin 有效。同一份样式写在 display: inline-block 上是 320 × 120(宽高都生效)。

padding / margin 简写

1
2
3
4
5
6
7
.x { padding: 10px; }                    /* 四边 */
.x { padding: 10px 20px; } /* 上下 10,左右 20 */
.x { padding: 10px 20px 30px; } /* 上10 左右20 下30 */
.x { padding: 10px 20px 30px 40px; } /* 上 右 下 左(顺时针) */
.x { margin-inline: auto; } /* 逻辑属性:横书写=左右,竖排书写=上下 */
.x { margin-block: 10px 20px; } /* 块方向(纵轴) */
.x { gap: 12px; } /* flex/grid 子项间距,实测可用于 flex */
  • 让定宽块元素水平居中:margin-inline: auto(等价 margin: 0 auto)。
  • gap 以前只有 grid 能用,实测现在 flex 也支持(gap: 12px 计算值生效),比用负 margin 补间距干净得多。

定位

position 五个值

值 是否占原位置 相对谁定位 典型用途
static 在流内 —(top/left 无效) 默认
relative 保留原位置占位 自己原来在流里的位置 局部微调、给子 absolute 当参照
absolute 脱离流 最近的非 static 祖先,没有则相对初始包含块 弹层、角标
fixed 脱离流 视口(除非祖先有 transform/filter/will-change 等) 常驻顶栏、返回顶部
sticky 在流内 滚动容器 + top/left/... 阈值 表格标题行、分组吸顶
1
2
3
4
.relative { position: relative; top: 10px; left: 20px; }   /* 向下 10、向右 20,原位置仍占着 */
.absolute { position: absolute; top: 30px; right: 40px; } /* 相对最近的定位祖先 */
.fixed-bar { position: fixed; bottom: 0; left: 0; right: 0; }
.header { position: sticky; top: 0; } /* 必须给一个偏移阈值才生效 */

sticky 实测(外层容器 height: 60px; overflow: auto,内含 position: sticky; top: 5px 的小条 + 300px 高内容):容器滚动 100px 后,sticky 条的视口位置保持 2429 → 2429 不变,而普通子元素从 2447 移到 2347(被内容一起滚走了)。这就是「粘住」的直观证据。

sticky 常见的「不生效」原因:

  1. 没写 top/bottom/left/right 任何一个阈值;
  2. 祖先里有 overflow: hidden/auto 之类的裁剪容器,粘性只在那个容器内起作用;
  3. sticky 元素的父容器高度就等于它自己(没有可粘的行进空间);
  4. 写了 display: inline-block(粘性定位对行内元素无效,块级或 flex 子项才有意义)。

inset 是四个偏移的简写

1
2
3
4
5
.overlay {
position: absolute;
inset: 4px; /* top right bottom left,实测计算值就是 "4px" */
}
.half { inset-block-start: 0; } /* 逻辑方向写法 */

z-index 与层叠上下文

1
2
.behind { position: absolute; z-index: 1; left: 0; top: 0; }
.in-front { position: absolute; z-index: 2; left: 50px; top: 50px; } /* 盖在上面 */
  • 数值大在上;同为 auto/相同数值时按文档顺序后来者居上。
  • z-index 只在同一个层叠上下文内比较,父级上下文先于子级排序——「我 z-index 写到 9999 还被别人盖住」几乎都是这个问题(父元素形成了更低层级的上下文)。
  • 原笔记说「只对定位元素有效」,已经过时:实测 display: flex 的两个静态子项(position: static),z-index: 3 的那个确实盖住了 z-index: 1 的那个(elementFromPoint 命中上层的 id)。flex/grid 子项的 z-index 同样参与叠放。
  • 会创建新层叠上下文的常见写法:
    • position 非 static 且 z-index 非 auto。
    • opacity < 1。
    • transform/filter/backdrop-filter/will-change。
    • position: fixed/sticky。
    • isolation: isolate。
    • flex / grid 的子项且 z-index 非 auto。
    • 想让某个「相对层级」独立出来,最干净的写法是 isolation: isolate。

水平垂直居中:四种做法

1
2
3
4
5
6
7
8
9
10
11
12
/* 1. 已知宽高:负 margin(老写法) */
.c1 { position: absolute; left: 50%; top: 50%; width: 200px; height: 100px; margin: -50px -100px; }

/* 2. 未知宽高:absolute + translate(实测元素中心与容器中心完全重合,dx=0 dy=0) */
#box-parent { position: relative; width: 300px; height: 200px; }
.c2 { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); }

/* 3. flex:容器上一行(align-items / justify-content 均为 center 时实测 dx=0 dy=0) */
.parent-flex { display: flex; align-items: center; justify-content: center; }

/* 4. grid:最省事的一行(实测 place-items 计算值就是 center) */
.parent-grid { display: grid; place-items: center; }
  • 只有「一行文字垂直居中」这种场景还适合用 line-height = height。
  • 单个块元素水平居中:margin-inline: auto + 固定/最大宽度。

布局

display 家族

值 行为 备注
block 独占一行,宽度撑满 div、p、h1… 默认(实测 display: block)
inline 跟着文字排,宽高与上下 margin/padding 无效 span、a、b…
inline-block 外部像行内、内部像块 实测 width: 300px 生效(320×120 含 padding)
flex 一维弹性布局 子项的 float/vertical-align 失效
grid 二维网格布局 子项同样不能用 float
flow-root 生成独立块格式上下文 实测可用,专门用来阻断 margin 合并
contents 盒子「消失」,子节点直接参与父级布局 实测有效,但会让该元素在无障碍树里也消失,慎用
table/table-cell 表格布局 除了真正的表格数据,别再拿它排版
none 完全不渲染 与 visibility: hidden(占位)不同
默认 overflow: visible 实测根元素计算值就是 visible 溢出内容照样绘制,可能与相邻元素重叠

Flexbox(一维)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.navbar {
display: flex; /* 开启 flex 布局 */
flex-direction: row; /* row / row-reverse / column / column-reverse */
justify-content: space-around; /* 主轴分布 */
align-items: center; /* 交叉轴对齐 */
flex-wrap: wrap; /* 换行:nowrap 是默认 */
align-content: space-between; /* 多行时行与行的分布(需 flex-wrap) */
gap: 12px 20px; /* 行间距 列间距 */
}

.navbar > li {
flex: 1 1 auto; /* grow shrink basis 三合一 */
align-self: flex-end; /* 单项覆盖交叉轴对齐 */
order: 2; /* 视觉顺序(不要用它做语义排序,读屏按 DOM 顺序) */
}
  • justify-content 可用值比原笔记列的多:normal、start、end、center、space-between、space-around、space-evenly、left、right。实测 space-evenly 生效(三者等距,含两端)。
  • align-items 除了 flex-start/flex-end/center/baseline/stretch,还能直接写 Box Alignment 的关键字:实测 align-items: end 有效。
  • flex: 1 是 flex: 1 1 0%,flex: auto 是 1 1 auto。想让一列按内容、其余列填满剩余,就写 flex: 0 0 auto + flex: 1。
  • flex-basis 才是伸缩的起点;写 width 也能,但 basis: 0 表示「忽略内容宽度重新分配」,做等分列更稳。
  • 三个高频坑:
    1. flex 子项里的文字要截断,必须给它 min-width: 0(默认 auto 不允许收缩到内容宽度以下)。
    2. 交叉轴高度想撑满用 align-items: stretch(默认值,但要求子项没写高度)。
    3. 用 gap 而不是 margin,避免最后一项多余外边距。

导航栏完整示例(把原笔记里非法的 <li> 收进 <ul>):

1
2
3
4
5
6
<ul class="navbar">
<li><a href="#">Home</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Services</a></li>
<li><a href="#">Contact</a></li>
</ul>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.navbar {
display: flex; /* 弹性容器 */
justify-content: space-around; /* 沿主轴均匀分布 */
align-items: center; /* 交叉轴居中 */
gap: 0 8px; /* 项目之间留最小间隙 */
margin: 0; /* 去掉列表默认外边距 */
padding: 10px 0; /* 上下内边距 */
list-style: none; /* 去掉项目符号 */
background-color: #333; /* 背景色 */
}

.navbar a {
padding: 5px 15px; /* 点击区域 */
color: white; /* 文字颜色 */
text-decoration: none; /* 去下划线 */
transition: background-color 0.3s; /* 悬停过渡 */
}

.navbar a:hover {
background-color: #555; /* 悬停底色 */
}

Grid(二维)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.layout {
display: grid;
grid-template-columns: 1fr 3fr; /* 两列,宽度比 1:3 */
grid-template-rows: auto 1fr auto; /* 三行,中间行吃掉剩余高度 */
gap: 10px; /* 行列间距 */
min-height: 100dvh; /* 至少占满视口 */
grid-template-areas:
"head head"
"side main"
"foot foot";
}

.layout > header { grid-area: head; } /* 用名字落位 */
.layout > aside { grid-area: side; }
.layout > main { grid-area: main; }
.layout > footer { grid-area: foot; }

.span-all {
grid-column: 1 / -1; /* 从第一条线到最后一条线 = 横跨所有列 */
}

.card { grid-column: span 2; } /* 跨两列 */
.gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); /* 响应式卡片墙,无需媒体查询 */
gap: 16px;
}
  • fr 是「剩余空间的一份」,只在 grid/flex 的轨道尺寸里可用。
  • repeat(auto-fit, minmax(200px, 1fr)) 是首选的自适应网格:容器变窄时自动减少列数,一行代码替掉三四个媒体查询。
  • align-items / justify-items 管格子里的对齐。
  • align-content / justify-content 管轨道整体在容器里的分布。
  • place-items 是前两者的简写(实测 place-items: center 生效)。单个项目还能用 justify-self / align-self 覆盖。
  • 原笔记担心 grid 的 justify-content: stretch 不合法:实测 Chrome 接受并保留为 stretch(grid 里它确实是个合法值,行为等同轨道拉伸)。
  • 子项默认会自动按行排列;不想手写行数列数就靠 grid-auto-flow: column 之类。
  • subgrid 实测支持(CSS.supports('grid-template-columns: subgrid') = true),可以做「卡片内的按钮对齐到同一列」。

老式 Float 布局(读旧代码要用)

1
2
3
4
5
6
7
8
9
10
11
12
.clearfix::after {
content: ""; /* 伪元素必须有 content 才会生成 */
display: block; /* 行内元素上的 clear 无意义 */
clear: both;
}

.col {
float: left;
width: 33.333%;
padding: 10px;
box-sizing: border-box; /* 否则 33.333% + padding 会挤不下三列 */
}
  • float 的本来用途是文字环绕图片,被拿来排版是历史偶然。
  • 浮动会使父容器高度塌陷(子元素脱离正常流,父容器「看不到」它们),所以要清除浮动:现代写法是给父容器 display: flow-root(实测可用,无需伪元素),比 .clearfix 更短。
  • 三列等分为什么必须配 box-sizing: border-box:33.333% + 10px padding × 2 在 content-box 下超过三分之一,第三列会被挤下去。

响应式

1
<meta name="viewport" content="width=device-width, initial-scale=1">
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 移动优先:基础样式写小屏,逐级覆盖 */
.box { width: 100%; }

@media (min-width: 601px) { /* 平板 */
.box { width: 50%; }
}

@media (min-width: 961px) { /* 桌面 */
.box { width: 33.333%; }
}

@media (prefers-color-scheme: dark) { body { background: #111; color: #eee; } }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; } }
@media print { nav, footer { display: none; } }
  • 必须写 viewport meta,否则手机上媒体查询按 980px 这类「布局视口」计算,断点看着像失效。
  • 断点建议按内容需要定,别照抄「手机 600 / 平板 960」。写 min-width(移动优先)比 max-width 更容易维护。
  • 原笔记在媒体查询里用 only screen,那是给老式浏览器兜底的写法,现在可省。
  • 媒体查询实测:当前视口 888px,matchMedia('(max-width: 600px)').matches = false、(min-width: 601px) = true;(prefers-color-scheme: dark) = false、(hover: hover) = true、(pointer: coarse) = false。
  • 内容驱动的自适应优先用这些「免断点」手段:clamp()、min()/max()、flex-wrap、grid + minmax(auto-fit)、aspect-ratio、object-fit、容器查询:
1
2
3
4
5
.card-container { container-type: inline-size; }      /* 实测支持(计算值 inline-size) */

@container (min-width: 400px) {
.card { display: grid; grid-template-columns: 120px 1fr; } /* 按容器宽度而不是屏幕宽度切换 */
}

布局方案怎么选

需求 选择
一行里的东西对齐、分布 flex
页面整体区块、卡片墙、需要行列同时对齐 grid
图文环绕 float(这才是它的本职)
局部脱流(角标、弹层) absolute
常驻工具条、吸顶 fixed / sticky
老项目维护 float + clearfix 或 flow-root

列表与表格样式

list-style 全家福

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ul.menu {
list-style: none; /* 最常用:去掉项目符号(连同缩进要再写 padding-left: 0) */
}

ul.doc {
list-style-type: square; /* 标记类型 */
list-style-position: inside; /* 标记放文字内侧 */
list-style-image: url("bullet.png"); /* 图片优先于类型;图片加载失败回退到 type */
}

ol.cn {
list-style-type: cjk-ideographic; /* 实测:一、二、三 */
list-style-type: simp-chinese-informal; /* 实测:一、二、三(口语数字) */
list-style-type: trad-chinese-formal; /* 壹、贰、叁 */
list-style-type: lower-greek; /* α、β、γ */
padding-left: 1.5em; /* 实测 ul 默认 padding-left = 40px */
}

li::marker { /* 只给标记上色,不动正文(实测可用) */
color: rgb(77, 88, 99);
font-weight: 700;
}

简写 list-style: circle inside url("bullet.png") 实测计算值拆出来正是 type=circle、position=inside、image=url("…/bullet.png")。

自定义编号还能用 @counter-style(实测 Chrome 支持 list-style-type 引用自定义样式),比如「A1、A2、B1…」这类复合序号,比自己拼 ::before 更规范。

用伪元素画自定义符号,比 list-style-image 更好控制对齐:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ul.tick {
list-style: none;
padding-left: 0;
}

ul.tick li {
position: relative;
padding-left: 1.5em; /* 给符号留位置 */
}

ul.tick li::before {
content: ""; /* 空 content 也要写,否则伪元素不存在 */
position: absolute;
left: 0;
top: 0.45em;
width: 8px;
height: 8px;
border-radius: 50%;
background: #06c;
}

表格样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
table {
border-collapse: collapse; /* 关键:合并边框,替代老式的 border + cellspacing=0 */
width: 100%;
table-layout: fixed; /* 固定布局:列宽按首行/设定值,比 auto 快且不会因内容乱挤 */
}

caption {
caption-side: bottom;
font-weight: 700;
padding: 8px;
}

th, td {
border: 1px solid #ccc;
padding: 6px 10px;
text-align: left;
vertical-align: top; /* 单元格里的垂直对齐用这个 */
}

thead th { background: #f0f0f0; }
tbody tr:nth-child(2n) { background: #fafafa; } /* 斑马纹 */
tbody tr:hover { background: #eef6ff; }
th[scope="row"] { text-align: left; font-weight: 600; }

/* 长表格:让表头行吸顶 */
thead th {
position: sticky;
top: 0;
}
  • 数字列用 text-align: right + font-variant-numeric: tabular-nums,对齐才好看。
  • 手机端横向滚动:外层套 overflow-x: auto 的容器(并给表格一个 min-width)。

动效与「现代 CSS」清单

过渡与动画最小可用版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.btn {
transition: background-color 0.3s ease, transform 0.15s ease; /* 只对能插值的属性做过渡 */
}

.btn:hover {
background-color: #06c;
transform: translateY(-2px);
}

@keyframes ticker {
from { transform: translateX(0); }
to { transform: translateX(-100%); }
}

.ticker span {
display: inline-block;
animation: ticker 10s linear infinite; /* 名称 时长 速度曲线 次数 */
}

@media (prefers-reduced-motion: reduce) {
.ticker span { animation: none; } /* 晕动症友好,值得养成习惯 */
}
  • 只动画 transform 和 opacity(GPU 友好,不触发布局);动画 width/top/left 会强制重排,明显掉帧。
  • transition 不生效的常见原因:写了 display: none(不可插值)、选择器没命中、属性拼错、值类型不一致(auto ↔ 100px)。
  • animation-play-state: paused、transition-behavior: allow-discrete(实测支持)可以解决「进场/退场动画」这类老难题。

本机 Chrome 实测支持情况(CSS.supports)

特性 实测
:has() 父选择器 支持
aspect-ratio: 1/1(320px 宽实测高 180px) 支持
flex 容器里的 gap 支持
color-mix(in oklab, …) 支持
container-type: inline-size(容器查询) 支持
dvh / svh 等动态视口单位 支持
text-wrap: balance(标题不再「一行一字」难看断行) 支持
field-sizing: content(输入框随内容变宽) 支持
backdrop-filter: blur()(毛玻璃) 支持
scroll-behavior: smooth 支持
overscroll-behavior: contain(阻止滚动链) 支持
inset 简写 / translate 独立属性 支持
accent-color(表单控件主题色,实测 rebeccapurple → rgb(102,51,153)) 支持
subgrid 支持
content-visibility: auto(跳过屏外渲染,长列表提速) 支持
::marker 样式 支持
list-style-type: cjk-ideographic 支持
lh 单位 支持
text-spacing-trim(中日韩标点压缩) 支持
transition-behavior 支持

其它值得记住的现代写法:

1
2
3
4
5
6
7
.grid { display: grid; grid-template-columns: repeat(12, 1fr); }
.ratio { aspect-ratio: 16 / 9; width: 320px; } /* 实测高 180px,替掉 padding-top 黑魔法 */
.img { object-fit: cover; } /* 图片在框内不变形裁切 */
.safe { width: min(92vw, 1100px); margin-inline: auto; } /* 限宽 + 居中一行搞定 */
.text { text-wrap: balance; } /* 标题断行更均匀 */
.pop { backdrop-filter: blur(8px); } /* 毛玻璃 */
.form-field { field-sizing: content; min-width: 6ch; } /* 输入框随内容自适应 */

常见坑速查

  • 样式没生效:优先级被压(行内 > id > class)、拼错属性名、值缺单位、选择器没命中(.a .b 与 .a.b)、元素是 display: inline 却给它写了宽高。
  • margin: 0 auto 不居中:元素没有设定宽度(auto 宽度会撑满,没有剩余空间可分配)。
  • 上下间距莫名变小:兄弟 margin 合并(实测 20 与 30 之间只剩 30);父容器高度塌陷则是子元素浮动,或子元素 margin-top 穿透。
  • width: 100% 却溢出:默认 box-sizing: content-box 时还要再加 padding/border。全局写 box-sizing: border-box。
  • position: absolute 跑到了页面外:祖先里没有一个 position 非 static 的元素。给最近的祖先加 relative。
  • position: fixed 跟着页面滚:某个祖先有 transform/filter/will-change,它就成了 fixed 的新参照物。
  • sticky 不粘:没写 top,或者滚动容器不是你以为的那个。
  • flex 子项里的长文字不出省略号:忘了 min-width: 0。
  • 圆角处背景溢出:overflow: hidden 或给子元素同样的 border-radius。
  • z-index 再大也被盖:不在同一个层叠上下文里比较;用 isolation: isolate 把父级独立出来。
  • hover 动画结束后抖动:动画里改了布局属性;只动 transform/opacity。
  • line-height: 1.6em 嵌套后行高失控:改用无单位 1.6。
  • 移动端媒体查询「不生效」:漏了 viewport meta。
  • 图片下方那条缝:行内元素基线留白,给 img { display: block; } 或 vertical-align: bottom/middle。
  • 写了 !important 才能盖住第三方库:这是允许的;但项目内部样式之间靠 !important 打架是设计问题,改用 @layer 或 CSS 变量。
  • 1px 边框在高倍屏显得粗:transform: scaleY(.5) 伪元素方案,或 border-width: thin。

练习

  • 找一个看上去顺眼的页面,试着复刻它的布局:京东、淘宝首页。要求先用 flex/grid,别用 float。
  • 给《HTML入门》里那张课程表加上样式:合并边框、斑马纹、悬停高亮、表头吸顶。
  • 用 clamp() + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)) 做一个不用媒体查询也能自适应的卡片墙。
  • 亲手复现一次 margin 合并:写两个相邻块元素,用 getBoundingClientRect() 量出它们之间的实际间距,再分别 padding-top: 1px、display: flow-root、overflow: hidden 打断它,观察差异。

参考链接

本文结束 感谢您的阅读
正在加载今日诗词....