普通视图

发现新文章,点击刷新页面。
今天 — 2025年11月20日首页

Gird快速入门(手把手敲demo)

作者 新晨437
2025年11月19日 21:02

Gird 布局是 CSS 中目前最强大、最成熟的二维布局方案,它可以让您轻松创建复杂的网页结构。

这份快速入门指南将带您从零开始,快速掌握 Grid 的核心概念和用法。

1. 什么是 CSS Grid?

CSS Grid Layout(网格布局)是一个二维的布局系统,意味着它可以同时处理。这与 Flexbox(一维布局,主要处理行列)形成鲜明对比。

核心思想:将一个容器划分为一个个网格,你可以将子元素放置在这些网格的任意位置,甚至可以创建复杂的、不对称的布局。


2. 基本概念

在开始之前,先了解两个核心概念:

  • Grid Container(网格容器):应用了 display: grid; 的元素。它的所有直接子元素将成为 Grid Item。
  • Grid Item(网格项目):Grid Container 的直接子元素
<div class="container"> <!-- Grid Container -->
  <div class="item">1</div> <!-- Grid Item -->
  <div class="item">2</div> <!-- Grid Item -->
  <div class="item">3</div> <!-- Grid Item -->
</div>

3. 快速启动:创建一个简单的网格

让我们通过一个简单的例子,一步步创建一个 3x3 的网格。

步骤 1:定义网格容器

首先,将一个元素的 display 属性设置为 gridinline-grid

.container {
  display: grid;
}

现在,.container 就成为了一个网格容器,它的直接子元素自动成为了网格项目。

步骤 2:划分行和列(定义网格轨道)

这是 Grid 布局的核心。我们使用 grid-template-columnsgrid-template-rows 属性来定义网格的结构。

  • grid-template-columns:定义每一列的宽度。
  • grid-template-rows:定义每一行的高度。

让我们定义一个 3 列(每列 100px)和 3 行(每行 100px)的网格:

.container {
  display: grid;
  grid-template-columns: 100px 100px 100px; /* 三列,每列100px */
  grid-template-rows: 100px 100px 100px;    /* 三行,每行100px */
}

更现代的写法:使用 repeat() 函数 当列或行很多时,repeat() 函数非常有用。

.container {
  display: grid;
  grid-template-columns: repeat(3, 100px); /* 等同于 100px 100px 100px */
  grid-template-rows: repeat(3, 100px);
}

灵活的写法:使用 fr 单位 fr(fraction)单位代表网格容器中的等分空间,它让布局变得非常灵活。

.container {
  display: grid;
  grid-template-columns: 1fr 2fr 1fr; /* 总宽度分为4份,第二列占2份,第一、三列各占1份 */
  grid-template-rows: repeat(3, 100px);
}

混合使用:

.container {
  display: grid;
  grid-template-columns: 200px 1fr 20%; /* 第一列固定200px,第三列是容器的20%,中间列占据剩余空间 */
}

步骤 3:网格间隙(Gap)

使用 gap 属性可以设置网格项目之间的间距。它是 row-gapcolumn-gap 的简写。

.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  gap: 10px; /* 行和列的间隙都是10px */
  /* 或者分开写: */
  /* row-gap: 15px; */
  /* column-gap: 10px; */
}

到此为止,一个标准的 3x3 网格就创建好了!你的 HTML 和 CSS 看起来应该是这样的:

<!DOCTYPE html>
<html lang="en">
<head>
  <style>
    .container {
      display: grid;
      grid-template-columns: repeat(3, 100px);
      grid-template-rows: repeat(3, 100px);
      gap: 10px;
      background-color: #f0f0f0;
      padding: 10px;
    }
    .item {
      background-color: #4CAF50;
      color: white;
      padding: 20px;
      text-align: center;
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="item">1</div>
    <div class="item">2</div>
    <div class="item">3</div>
    <div class="item">4</div>
    <div class="item">5</div>
    <div class="item">6</div>
    <div class="item">7</div>
    <div class="item">8</div>
    <div class="item">9</div>
  </div>
</body>
</html>

4. 高级操作:放置网格项目

默认情况下,项目会按顺序逐行逐列填充网格。但 Grid 的强大之处在于你可以精确控制每个项目的位置。

我们通过 网格线(Grid Lines) 来定位项目。网格线是划分网格的线,从 1 开始编号(也从 -1 开始倒序编号)。

CSS Grid Lines转存失败,建议直接上传图片文件

使用以下属性来控制项目位置:

  • grid-column-start
  • grid-column-end
  • grid-row-start
  • grid-row-end
  • 简写:grid-columngrid-row

示例:让第一个 item 横跨两列

.item:nth-child(1) {
  grid-column-start: 1;
  grid-column-end: 3; /* 从第1条纵线开始,到第3条纵线结束 */
  /* 简写: */
  /* grid-column: 1 / 3; */
}

示例:创建一个页面的经典布局

<div class="container">
  <header>Header</header>
  <aside>Sidebar</aside>
  <main>Main Content</main>
  <footer>Footer</footer>
</div>
.container {
  display: grid;
  grid-template-columns: 200px 1fr; /* 侧边栏固定宽度,主内容区自适应 */
  grid-template-rows: auto 1fr auto; /* 页头和页脚高度由内容决定,主内容区占据剩余空间 */
  height: 100vh; /* 让容器充满整个视口高度 */
  gap: 10px;
}

header {
  grid-column: 1 / -1; /* 从第1条纵线开始,到最后一条纵线结束(横跨所有列) */
  background: lightblue;
}

aside {
  grid-column: 1 / 2; /* 占据第一列 */
  background: lightcoral;
}

main {
  grid-column: 2 / 3; /* 占据第二列 */
  background: lightgreen;
}

footer {
  grid-column: 1 / -1; /* 横跨所有列 */
  background: lightgray;
}

5. 快速总结与最佳实践

  1. 启动display: grid;
  2. 定义结构
    • grid-template-columns: 定义列。
    • grid-template-rows: 定义行。
    • 善用 repeat()fr 单位。
  3. 设置间距gap
  4. 精细控制项目:使用 grid-columngrid-row 基于网格线进行定位。
  5. 命名网格线:对于复杂布局,可以给网格线起名字,让代码更易读。
    .container {
      grid-template-columns: [sidebar-start] 200px [sidebar-end content-start] 1fr [content-end];
    }
    header {
      grid-column: sidebar-start / content-end;
    }
    

练习建议

打开浏览器的开发者工具,在 ElementsInspector 面板中,找到你的网格容器,旁边会有一个 grid 的标签,点击它可以在页面上可视化你的网格结构。这是学习和调试 Grid 的绝佳方式!

Grid 布局功能非常丰富,还包括网格区域(grid-template-areas)、对齐属性(justify-items, align-content等),但掌握了以上核心内容,你已经可以应对 90% 的布局需求了。快去动手试试吧!

昨天 — 2025年11月19日首页

Gird布局详解

作者 嬉皮客
2025年11月18日 21:16

一、什么是 Grid 布局?

CSS Grid Layout(网格布局)是 CSS 提供的二维布局系统,可以同时在行(row)和列(column)两个方向上对元素进行排列。
它和 Flexbox 不同,Flexbox 是一维布局(只能按行或列排),而 Grid 是二维布局,可以轻松实现复杂的跨行、跨列布局。

特点:

  • 二维布局:同时控制行和列,对应一维布局复杂度也随之增加
  • 跨行跨列:元素可以跨多个单元格,一维度布局需要层层嵌套来做这些效果
  • 语义清晰grid-template-areas 像地图一样描述布局,布局就像ASCLL表一样清晰直观
  • 响应式友好:只改 Grid 定义,不动 HTML,就能重排布局,经常配合@media使用
  • 现代浏览器支持好:桌面端和移动端几乎全覆盖(IE11 部分支持旧语法),现代浏览器基本都是支持的,排除一下老旧项目(政府、银行等)还在使用

二、基本概念

容器和项目

什么是容器?

设置了 display: grid 或 display: inline-grid 的元素,就是 Grid 容器,容器的直接子元素就是 Grid 项目。容器负责定义整个网格的结构(行、列、间距、对齐方式等)。

什么是项目?

Grid 容器的直接子元素就是 Grid 项目,项目可以通过属性控制它在网格中的位置、跨行跨列、对齐方式等。

行和列

容器里面的水平区域称为"行"(row),垂直区域称为"列"(column)。

下图水平深色区域就是“行”,垂直深色区域就是“列”

image.png

单元格

行和列的交叉区域,称为"单元格"(cell),n行和m列会产生n x m个单元格。比如,3行3列会产生9个单元格。

网格线

划分网格的线,称为"网格线"(grid line)。水平网格线划分出行,垂直网格线划分出列,n行有n + 1根水平网格线,m列有m + 1根垂直网格线,比如三行就有四根水平网格线。

如下图就有5根水平网格线和5根垂直网格线

image.png

三、容器属性

display:grid | inline-grid

作用:指定一个容器采用网格布局

语法:

.container{
    display: grid | inline-grid;
}

注意:设为网格布局以后,容器子元素(项目)的floatdisplay: inline-blockdisplay: table-cellvertical-aligncolumn-*等设置都将失效。

grid-template-columns与grid-template-rows

作用:grid-template-columns属性定义每一列的列宽,grid-template-rows属性定义每一行的行高

实例:指定了一个三行三列的网格,列宽和行高都是100px

image.png

.container {
  display: grid;
  grid-template-columns: 100px 100px 100px;
  grid-template-rows: 100px 100px 100px;
}

.container > div {
  width: 60px;
  height: 60px;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
<div class="container">
    <div>1</div>
    <div>2</div>
    <div>3</div>
    <div>4</div>
    <div>5</div>
    <div>6</div>
    <div>7</div>
    <div>8</div>
    <div>9</div>
</div>
repeat()

作用:使用repeat()函数,简化重复的值,接受两个参数,第一个参数是重复的次数,第二个参数是所要重复的值,也可以重复某个模式

实例:

  • 简单重复值

image.png

.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
}
.container > div {
  width: 100%;
  height: 100%;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
  • 重复某个模式

image.png

.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
}
.container > div {
  width: 100%;
  height: 100%;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
auto-fill与auto-fit

作用:单元格的大小是固定的,但是容器的大小不确定。如果希望每一行(或每一列)容纳尽可能多的单元格,这时可以使用auto-fill关键字表示自动填充,只有当容器足够宽,可以在一行容纳所有单元格,并且单元格宽度不固定的时候,才会有差异:auto-fill会用空格子填满剩余宽度,auto-fit则会尽量扩大单元格的宽度

实例:

例子中宽设置了400px,高设置200px,属性auto-fill,高400px/100px=4个格子,宽200px/100px=2个格子,所以超出的第九个被空放了

image.png

.container {
  width: 400px;
  height: 200px;
  display: grid;
  grid-template-columns: repeat(auto-fill, 100px);
  grid-template-rows: repeat(auto-fill, 100px);
}
fr

作用:为了方便表示比例关系,网格布局提供了fr关键字(fraction 的缩写,意为"片段")。如果两列的宽度分别为1fr2fr,就表示后者是前者的两倍

实例:

image.png

.container {
  width: 400px;
  display: grid;
  grid-template-columns: 300px 1fr 2fr;
  grid-template-rows: repeat(3, 100px);
}
minmax

作用:minmax()函数产生一个长度范围,表示长度就在这个范围之中。它接受两个参数,分别为最小值和最大值

实例:

每行第三列位置宽度100px~2fr

image.png

.container {
  display: grid;
  grid-template-columns: 1fr 1fr minmax(100px, 2fr);
  grid-template-rows: repeat(3, 100px);
}
auto

作用:由浏览器自己决定长度

实例:

image.png

.container {
  width: 400px;
  display: grid;
  grid-template-columns: 100px auto 100px;
  grid-template-rows: repeat(3, 100px);
}
自定义网格线名称

作用:指定每一根网格线的名字,方便以后的引用

实例:

指定网格布局为3*3,因此有4根垂直网格线和4根水平网格线,每个网格线可以指定多个名字,比如[r4 xipiker666]

.container {
  display: grid;
  grid-template-columns: [c1] 100px [c2] 100px [c3] auto [c4];
  grid-template-rows: [r1] 100px [r2] 100px [r3] auto [r4 xipiker666];
}

grid-columns-gap(columns-gap)与grid-rows-gap(rows-gap)与grid-gap(gap)

作用:grid-columns-gap设置列间距、grid-rows-gap设置行间距,可以分别简写为columns-gaprows-gapgrid-gap设置行列间距grid-gap: <行间距> <列间距>grid-gap可以简写为gap,如果行和列的值相等可以gap: xxpx

实例:

image.png

.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  gap: 20px;
}

grid-template-areas

作用:网格布局允许指定"区域"(area),一个区域由单个或多个单元格组成

实例:

  • 划分出9个单元格
.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  grid-template-areas: 
    'a b c'
    'd e f'
    'g h i';                 
  gap: 20px;  
}
  • 多个单元格合并成一个区域
.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  grid-template-areas: 
    'a a a'
    'b b b'
    'c c c';                 
  gap: 20px;  
}
  • 某些区域不需要利用,则使用.表示
.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  grid-template-areas: 
    'a . a'
    'b . b'
    'c . c';                 
  gap: 20px;  
}

grid-auto-flow

作用:容器的子元素的排序方式,属性值row先行后列,属性值columns先列后行,属性值row dense子项目指定位置后再先行后列排序,属性值columns dense子项目指定位置后再先列后行排序

实例:

  • row先行后列

image.png

.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  grid-auto-flow: row;
}
  • columns先列后行

image.png

.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  grid-auto-flow: column;
}
  • row dense子项目指定位置后先行后列

image.png

.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  grid-auto-flow: row dense;
}

.item_first {
  grid-column-start: 1;
  grid-column-end: 3;
}

.item_two {
  grid-column-start: 1;
  grid-column-end: 3;
}

.container > div {
  width: 100%;
  height: 100%;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
  • columns dense子项目指定位置后先列后行

image.png

.container {
  width: 400px;
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  grid-auto-flow: column dense;
}

.item_first {
  grid-column-start: 1;
  grid-column-end: 3;
}

.item_two {
  grid-column-start: 1;
  grid-column-end: 3;
}

.container > div {
  width: 100%;
  height: 100%;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}

justify-items与align-items与place-items

作用:justify-items属性设置单元格内容的水平位置,align-items属性设置单元格内的垂直位置,place-items属性是justify-itemsalign-items的组合属性

实例:

  • justify-items: start | center | end | stretch

image.png

.container {
  width: 300px;
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  justify-items: center;
}

.container > div {
  width: 60px;
  height: 60px;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
  • align-items: start | center | end | stretch

image.png

.container {
  width: 300px;
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  align-items: center;
}

.container > div {
  width: 60px;
  height: 60px;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
  • place-items: <align-items> <justify-items>

image.png

.container {
  width: 300px;
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  place-items: center end;
}

.container > div {
  width: 60px;
  height: 60px;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}

justify-content与align-content与place-content

作用:justify-content属性设置内容区域在容器里面的水平位置,align-content属性设置内容区域在容器里面的垂直位置,place-content属性是justify-contentalign-content的组合属性

实例:

  • justify-content: start | end | center | stretch | space-around | space-between | space-evenly

image.png

.container {
  width: 600px;
  background: #f9f9f9;
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  justify-content: center;
}

.container > div {
  width: 60px;
  height: 60px;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
  • align-content: start | end | center | stretch | space-around | space-between | space-evenly

image.png

.container {
  width: 600px;
  height: 400px;
  background: #f9f9f9;
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  align-content: center;
}

.container > div {
  width: 60px;
  height: 60px;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
  • place-content: <align-content> <justify-content>

image.png

.container {
  width: 600px;
  height: 400px;
  background: #f9f9f9;
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  place-content: center center;
}

grid-auto-columns与grid-auto-rows

作用:可以理解为,现有网格的外部设置多余的网格,用来放超出的子项目

实例:

  • grid-auto-columns

image.png

.container {
  background: #f9f9f9;
  display: grid;
  grid-template-columns: 100px 100px;
  grid-template-rows: 50px;
  grid-auto-rows: 80px
  grid-auto-flow: row; /*这里要设置按照行方向填充,可以不设置,因为默认按照行方向*/
}
  • grid-auto-rows

image.png

.container{
  background: #f9f9f9;
  display: grid;
  grid-template-rows: 50px 50px;
  grid-template-columns: 100px;
  grid-auto-columns: 150px; 
  grid-auto-flow: column; /*注意这里要设置按照列方向填充,必须设置,因为默认按照行防线*/
}

grid-template与grid

作用:grid-template属性是grid-template-columnsgrid-template-rowsgrid-template-areas这三个属性的合并简写形式,grid属性是grid-template-rowsgrid-template-columnsgrid-template-areas、 grid-auto-rowsgrid-auto-columnsgrid-auto-flow这六个属性的合并简写形式

四、项目属性

grid-column-start、grid-column-end与grid-row-start、grid-row-end

作用:可以理解为设置项目占多少个网格线,分别定位在哪根网格线

实例:

  • grid-column-start: <number>左边框所在的垂直网格线、grid-column-end: <number>右边框所在的垂直网格线

image.png

.container {
  background: #f9f9f9;
  display: grid;
  grid-template-rows: repeat(3, 100px);
  grid-template-columns: repeat(3, 100px);
}

.item_first {
  grid-column-start: 1;
  grid-column-end: 3;
}

.container > div {
  width: 100%;
  height: 100%;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
<div class="container">
    <div className="item_first">1</div>
    <div>2</div>
    <div>3</div>
    <div>4</div>
    <div>5</div>
    <div>6</div>
    <div>7</div>
    <div>8</div>
    <div>9</div>
</div>
  • grid-row-start: <number>上边框所在的垂直网格线、grid-row-end: <number>下边框所在的垂直网格线

image.png

.container {
  background: #f9f9f9;
  display: grid;
  grid-template-rows: repeat(3, 100px);
  grid-template-columns: repeat(3, 100px);
}

.item_first {
  grid-row-start: 1;
  grid-row-end: 3;
}

.container > div {
  width: 100%;
  height: 100%;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
<div class="container">
    <div className="item_first">1</div>
    <div>2</div>
    <div>3</div>
    <div>4</div>
    <div>5</div>
    <div>6</div>
    <div>7</div>
    <div>8</div>
    <div>9</div>
</div>

grid-column与grid-row

作用:grid-column属性是grid-column-startgrid-column-end的简写形式、grid-row属性是grid-row-startgrid-row-end的简写形式

实例:

  • grid-column: <grid-column-start> / <grid-column-end>
  • grid-row: <grid-row-start> / <grid-row-end>

grid-area

作用:指定项目放在哪个区域,grid-area属性还可用作grid-row-startgrid-column-startgrid-row-endgrid-column-end的合并简写形式,直接指定项目的位置

实例:

  • 指定放在哪个区域

image.png

.container {
  background: #f9f9f9;
  display: grid;
  grid-template-rows: repeat(3, 100px);
  grid-template-columns: repeat(3, 100px);
  grid-template-areas:
    'a b c'
    'd e f'
    'g h i';
}

.item_first {
  grid-area: e;
}

.container > div {
  width: 100%;
  height: 100%;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
  • grid-area: <row-start> / <column-start> / <row-end> / <column-end>

image.png

@pos: ~'1 / 1 / 3 / 3';

.container {
  background: #f9f9f9;
  display: grid;
  grid-template-rows: repeat(3, 100px);
  grid-template-columns: repeat(3, 100px);
}

.item_first {
  grid-area: @pos;
}

.container > div {
  width: 100%;
  height: 100%;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}

justify-self与align-self与place-self

作用:justify-self属性设置单元格内容的水平位置、align-self属性设置单元格内容的垂直位置,place-self属性是justify-selfalign-self的属性组合

实例:

image.png

.container {
  background: #f9f9f9;
  display: grid;
  grid-template-rows: repeat(3, 100px);
  grid-template-columns: repeat(3, 100px);
}

.item_first {
  justify-self: center;
  align-self: center;
}

.container > div {
  width: 60px;
  height: 60px;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}

五、常见布局案例

三列等宽布局

效果

image.png

代码

.container {
  background: #f9f9f9;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}

.container > div {
  width: 100%;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
<div className="container">
    <div>1</div>
    <div>2</div>
    <div>3</div>
</div>

圣杯布局

效果

image.png

代码

.container {
  display: grid;
  grid-template-areas:
    'header header'
    'sidebar main'
    'footer footer';
  grid-template-columns: 200px 1fr;
  grid-template-rows: 60px 1fr 40px;
  gap: 10px;
}
.header {
  grid-area: header;
  background: lightblue;
}
.sidebar {
  grid-area: sidebar;
  background: lightgoldenrodyellow;
}
.main {
  grid-area: main;
  background: lightgreen;
}
.footer {
  grid-area: footer;
  background: lightpink;
}
<div class="container">
    <div className="header">header</div>
    <div className="sidebar">sidebar</div>
    <div className="main">main</div>
    <div className="footer">footer</div>
</div>

媒体查询响应式布局(Gird+Flex最佳拍档)

效果

屏幕宽度小于等于660px

image.png

屏幕宽度小于等于1024

image.png

屏幕宽度大于1024

image.png代码

<div class="dashboard">
    <div class="stats">Stats</div>
    <div class="overview">Overview</div>
    <div class="lifecycle">Lifecycle</div>
    <div class="calendar">Calendar</div>
    <div class="monitor">Monitor</div>
    <div class="ladder">Ladder</div>
    <div class="stage">Stage</div>
</div>
.dashboard {
  display: grid;
  gap: 20px;
  padding: 20px;
  /* 桌面端布局 */
  grid-template-columns: 2fr 2fr 1fr;
  grid-template-areas:
    'stats stats stats'
    'overview lifecycle calendar'
    'monitor ladder calendar'
    'stage stage stage';
}

.dashboard > div {
  background: #eee;
  padding: 10px;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.2rem;
  font-weight: bold;
}

.stats {
  grid-area: stats;
}
.overview {
  grid-area: overview;
}
.lifecycle {
  grid-area: lifecycle;
}
.calendar {
  grid-area: calendar;
}
.monitor {
  grid-area: monitor;
}
.ladder {
  grid-area: ladder;
}
.stage {
  grid-area: stage;
}

@media (max-width: 1024px) {
  .dashboard {
    grid-template-columns: 1fr 1fr;
    grid-template-areas:
      'stats stats'
      'overview lifecycle'
      'calendar calendar'
      'monitor ladder'
      'stage stage';
  }
}

@media (max-width: 600px) {
  .dashboard {
    grid-template-columns: 1fr;
    grid-template-areas:
      'stats'
      'overview'
      'lifecycle'
      'calendar'
      'monitor'
      'ladder'
      'stage';
  }
}

Grid瀑布流布局

效果

image.png

代码

.container {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  grid-auto-rows: 10px;
  grid-auto-flow: dense;
  gap: 10px;
}
.item {
  background: lightblue;
}
.item1 {
  grid-row-end: span 15;
}
.item2 {
  grid-row-end: span 25;
}
.item3 {
  grid-row-end: span 20;
}
<div class="container">
    <div class="item item1">1</div>
    <div class="item item2">2</div>
    <div class="item item3">3</div>
    <div class="item item1">4</div>
</div>

六、总结

上述内容主要参考,大家可以参考原文,如果能帮到您,欢迎点赞、收藏、+关注

CSS Grid 网格布局教程 - 阮一峰的网络日志

昨天以前首页

element-plus主题配置及动态切换主题

作者 天外来物
2025年11月18日 14:02

创建vue项目的不同方式

pnpm create vite 使用于创建一些组件库,第三方库的时候

pnpm create vue 适用于vue项目,内部有一些基础的样式,vue的主题,

element-plus主题配置

按需引入element

1. scss变量自定义主题

分支:feature-element-theme-anxu-scss

src/styles/element/index.scss

/* just override what you need */
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
  $colors: (
    'primary': (
      'base': green,
    ),
  )
);

vite.config.ts

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import ElementPlus from 'unplugin-element-plus/vite'

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueDevTools(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [
        ElementPlusResolver({
          importStyle: 'sass',
        }),
      ],
    }),
    ElementPlus({
      useSource: true,
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use "@/styles/element/index.scss" as *;`,
      },
    },
  },
})

按需引入组件时,使用组件,会自动引入对应组件的样式,不用再main.ts中引入elememnt的全部样式。

主题配置只需要修改需要改变的颜色变量即可

可以看到,对应组件的颜色变量已经变化

image.png

2. css变量自定义主题

分支: feature-element-theme-anxu-css

src/styles/element/index.scss

:root {
  --el-color-primary: green;
}

vite.config.ts

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import ElementPlus from 'unplugin-element-plus/vite'

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueDevTools(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [
        ElementPlusResolver({
          importStyle: 'sass',
        }),
      ],
    }),
    ElementPlus({
      useSource: true,
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use "@/styles/element/index.scss" as *;`,
      },
    },
  },
})

可以看到,css变量被注入到组件对应的scss文件顶部,从而让主题生效

image.png

全量引入element

scss全量引入

分支 feature-element-theme-all-scss

/* just override what you need */
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
  $colors: (
    'primary': (
      'base': green,
    ),
  )
);

// If you just import on demand, you can ignore the following content.
// 如果你是全量导入,需要加下边这句,如果是按需引入,请注释掉下边这句
@use 'element-plus/theme-chalk/src/index.scss' as *;

main.ts

import './assets/main.css'

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import './styles/element/index.scss'
import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(createPinia())
app.use(router)

app.use(ElementPlus)
app.mount('#app')

这时候可以看到样式被全部引入了

image.png

css全量引入

分支:feature-element-theme-all-css src/styles/element/index.css

:root {
  --el-color-primary: green;
}

main.ts

import './assets/main.css'

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import './styles/element/index.css'
import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(createPinia())
app.use(router)

app.use(ElementPlus)
app.mount('#app')

可以看到,element的css变量已经被覆盖了

image.png

动态主题

运行时动态切换主题,不能依赖scss变量

原因

SCSS 变量是编译时变量,在构建阶段就被替换掉了,在浏览器运行时无法再改变,所以无法用于“动态切换主题”。

动态主题应该使用css变量来实现

官方提供了一个切换主题的项目github.com/element-plu…

里面引入了一套暗黑模式的样式,然后通过 useToggle进行切换

分支 feature-element-theme-anxu-scss-dynamic 还是采用按需引入elememnt的方式

src/styles/index.scss 这里全量引入暗黑模式的样式

// import dark theme
@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *;

// :root {
//   --ep-color-primary: red;
// }

body {
  font-family:
    Inter, system-ui, Avenir, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
    'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  margin: 0;
}

a {
  color: var(--ep-color-primary);
}

main.ts在main.ts中引入样式

// import './assets/main.css'

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import '@/styles/index.scss'

const app = createApp(App)

app.use(createPinia())
app.use(router)

app.mount('#app')

src/styles/element/index.scss 这里是自定义默认主题变量

$--colors: (
  'primary': (
    'base': rgb(0, 128, 19),
  ),
  'success': (
    'base': #21ba45,
  ),
  'warning': (
    'base': #f2711c,
  ),
  'danger': (
    'base': #db2828,
  ),
  'error': (
    'base': #db2828,
  ),
  'info': (
    'base': #42b8dd,
  ),
);

// You should use them in scss, because we calculate it by sass.
// comment next lines to use default color
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
  // do not use same name, it will override.
  $colors: $--colors // $button-padding-horizontal: ('default': 50px)
);

// if you want to import all
// @use "element-plus/theme-chalk/src/index.scss" as *;

// You can comment it to hide debug info.
// @debug $--colors;

// custom dark variables
@use './dark.scss';

src/styles/element/dark.scss 这是对暗黑主题颜色重新定义

// only scss variables

$--colors: (
  'primary': (
    'base': #589ef8,
  ),
);

@forward 'element-plus/theme-chalk/src/dark/var.scss' with (
  $colors: $--colors
);

vite.config.ts 这里引入自定义主题变量,自定义默认主题和暗黑主题颜色

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import ElementPlus from 'unplugin-element-plus/vite'

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueDevTools(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [
        ElementPlusResolver({
          importStyle: 'sass',
        }),
      ],
    }),
    ElementPlus({
      useSource: true,
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use "@/styles/element/index.scss" as *;`,
      },
    },
  },
})

动态切换主题

  • 通过useDark获取当前是否是暗黑模式
  • 通过useToggle切换主题
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import { useDark, useToggle } from '@vueuse/core'
const isDark = useDark()
const toggleDark = useToggle(isDark)
const changeTheme = () => {
  toggleDark()
}
</script>

<template>
  <header>
    <el-button type="primary" @click="changeTheme"> 切换主题 </el-button>
    <el-input></el-input>
  </header>

  <RouterView />
</template>

<style scoped>
header {
  line-height: 1.5;
  max-height: 100vh;
}

.logo {
  display: block;
  margin: 0 auto 2rem;
}

nav {
  width: 100%;
  font-size: 12px;
  text-align: center;
  margin-top: 2rem;
}

nav a.router-link-exact-active {
  color: var(--color-text);
}

nav a.router-link-exact-active:hover {
  background-color: transparent;
}

nav a {
  display: inline-block;
  padding: 0 1rem;
  border-left: 1px solid var(--color-border);
}

nav a:first-of-type {
  border: 0;
}

@media (min-width: 1024px) {
  header {
    display: flex;
    place-items: center;
    padding-right: calc(var(--section-gap) / 2);
  }

  .logo {
    margin: 0 2rem 0 0;
  }

  header .wrapper {
    display: flex;
    place-items: flex-start;
    flex-wrap: wrap;
  }

  nav {
    text-align: left;
    margin-left: -1rem;
    font-size: 1rem;

    padding: 1rem 0;
    margin-top: 1rem;
  }
}
</style>

代码仓库地址 github.com/Stacey1018/…

CSS 像素≠物理像素:0.5px 效果的核心密码是什么?

2025年11月14日 17:02

先明确两者的关系:CSS 像素是 “逻辑像素”(页面布局用),物理像素是屏幕实际发光的像素点,两者通过 设备像素比(DPR)  关联,公式为:1 个 CSS 像素 = DPR × DPR 个物理像素(仅高清屏缩放为 1 时)。

理解这个核心关系后,再看 0.5px 效果的实现逻辑就更清晰了,以下重新整理(重点补充像素关系,再对应方法):

一、先搞懂:CSS 像素、物理像素、DPR 的核心关系

  1. 定义

    • CSS 像素:写代码时用的单位(如 width: 100px),是浏览器渲染布局的 “逻辑单位”,和屏幕硬件无关。

    • 物理像素:屏幕面板上实际的发光点(如手机屏分辨率 1080×2340,就是横向 1080 个、纵向 2340 个物理像素),是屏幕的硬件属性。

    • DPR(设备像素比):DPR = 物理像素宽度 / CSS 像素宽度(默认页面缩放为 1 时),由设备硬件决定。

      • 例 1:老款普通屏(DPR=1):1 个 CSS 像素 = 1×1 个物理像素(写 1px 就对应屏幕 1 个发光点)。
      • 例 2:高清屏(DPR=2,如 iPhone 8):1 个 CSS 像素 = 2×2 个物理像素(写 1px 实际占用屏幕 4 个发光点,视觉上更粗)。
      • 例 3:超高清屏(DPR=3,如 iPhone 14 Pro):1 个 CSS 像素 = 3×3 个物理像素(写 1px 占用 9 个发光点,更粗)。
  2. 关键结论

    • 我们想要的 “0.5px 效果”,本质是 让线条只占用 1 个物理像素(视觉上最细)。
    • 但高清屏(DPR≥2)默认下,1 个 CSS 像素会占用多个物理像素,所以不能直接写 1px,需要通过方法 “压缩” CSS 像素对应的物理像素数量,最终落到 1 个物理像素上。

二、按 DPR 要求分类的 0.5px 实现方法(结合像素关系)

(一)仅 DPR≥2 生效:直接让 CSS 像素对应 1 个物理像素

核心逻辑:利用 DPR≥2 的像素映射关系,让 CSS 像素经过计算后,刚好对应 1 个物理像素。

1. 直接声明 0.5px
  • 像素关系:DPR=2 时,0.5px CSS 像素 = 0.5×2 = 1 个物理像素(刚好满足需求);DPR=3 时,0.5px CSS 像素 = 0.5×3 = 1.5 个物理像素(接近细线条,视觉可接受)。
  • 前提:DPR≥2 + 浏览器支持亚像素渲染(iOS 9+、Android 8.0+)。
  • 代码border: 0.5px solid #000;
  • 局限:DPR=1 时,0.5px CSS 像素 = 0.5×1 = 0.5 个物理像素(屏幕无法渲染,会四舍五入为 0px 或 1px)。
2. transform: scale(0.5) 缩放
  • 像素关系:先写 1px CSS 像素(DPR=2 时对应 2 个物理像素),再缩放 50%,最终 2×50% = 1 个物理像素。

  • 前提:DPR≥2(只有 DPR≥2 时,1px CSS 像素才会对应 ≥2 个物理像素,缩放后才能落到 1 个)。

  • 代码

    .line::after {
      content: '';
      width: 200%;
      height: 1px; /* 1px CSS = 2 物理像素(DPR=2) */
      background: #000;
      transform: scale(0.5); /* 2 物理像素 × 0.5 = 1 物理像素 */
    }
    
  • 局限:DPR=1 时,1px CSS 像素 = 1 物理像素,缩放后变成 0.5 物理像素(屏幕无法渲染,线条消失或模糊)。

3. viewport 缩放(全局方案)
  • 像素关系:通过 initial-scale=1/DPR 改变页面缩放比例,让 1px CSS 像素直接对应 1 个物理像素。

    • 例:DPR=2 时,缩放 50%(1/2),此时 1px CSS 像素 = 1 物理像素(原本 2 物理像素,缩放后压缩为 1);DPR=3 时,缩放 33.3%(1/3),1px CSS 像素 = 1 物理像素。
  • 前提:DPR≥2(高清屏),需配合布局单位(如 rem)调整。

  • 代码

    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <script>
      const dpr = window.devicePixelRatio || 1;
      document.querySelector('meta[name="viewport"]').setAttribute('content', 
        `width=device-width, initial-scale=${1/dpr}, user-scalable=no`
      );
    </script>
    
  • 优势:直接写 border: 1px 就是 1 物理像素,适配所有 DPR≥2 的设备。

  • 局限:全局缩放会影响布局,需重新计算 rem 基准值(如 html { font-size: 16px * dpr })。

(二)DPR≥2 最优,DPR=1 可模拟:视觉层面实现 “细于 1px”

核心逻辑:不依赖像素映射的精准计算,而是通过视觉欺骗或矢量渲染,让线条看起来比 1px 细(DPR=1 时无法实现 1 物理像素,只能模拟)。

1. SVG 绘制
  • 像素关系:SVG 是矢量图,不依赖 CSS 像素和物理像素的映射,直接按 “坐标 + 线条宽度” 渲染。

    • DPR≥2 时:stroke-width="1" + y1="0.5" 直接渲染为 1 个物理像素(矢量渲染支持亚像素精准控制)。
    • DPR=1 时:同样的代码会渲染为 “视觉上 0.5px 细的线条”(实际还是 1 物理像素,但矢量缩放让边缘更细腻,比直接写 1px 看起来细)。
  • 前提:无严格 DPR 要求,所有支持 SVG 的浏览器(几乎所有移动端)。

  • 代码

    <svg width="100%" height="1" xmlns="http://www.w3.org/2000/svg">
      <line x1="0" y1="0.5" x2="100%" y2="0.5" stroke="#000" stroke-width="1" />
    </svg>
    
2. 背景渐变(background-image
  • 像素关系:利用 1px 高的 CSS 容器,通过颜色分割模拟 “半像素”。

    • DPR=2 时:1px CSS 容器 = 2 物理像素高,渐变 “透明 50% + 有色 50%” 刚好对应 1 个物理像素的有色线条。
    • DPR=1 时:1px CSS 容器 = 1 物理像素高,渐变后视觉上是 “半透明细线”(比纯 1px 细,但本质是 1 物理像素的颜色叠加)。
  • 前提:支持 CSS3 渐变的浏览器(iOS 7+、Android 4.4+)。

  • 代码

    .line {
      height: 1px;
      background: linear-gradient(to bottom, transparent 50%, #000 50%);
    }
    
3. box-shadow 模拟
  • 像素关系:DPR=2 时,box-shadow: 0 0.5px 0 #000 中,0.5px CSS 偏移量 = 1 物理像素,形成 1 物理像素的细阴影(视觉上是细线条)。
  • 前提:DPR≥2(DPR=1 时,0.5px 偏移 = 0.5 物理像素,屏幕无法渲染,阴影不显示或模糊)。
  • 代码box-shadow: 0 0.5px 0 #000;

三、最终总结(结合像素关系)

实现方式 像素映射逻辑(核心) 依赖 DPR 视觉效果
直接 0.5px DPR≥2 时,0.5px CSS = 1 物理像素 DPR≥2 精准细线条
transform: scale DPR≥2 时,1px CSS(2 物理像素)缩放 50% = 1 物理像素 DPR≥2 兼容性好,精准细线条
viewport 缩放 DPR≥2 时,缩放 1/DPR 让 1px CSS = 1 物理像素 DPR≥2 全局适配,精准细线条
SVG 绘制 矢量渲染,直接控制 1 物理像素(DPR≥2)或模拟细线条(DPR=1) 无(DPR≥2 最优) 跨设备,细腻无模糊
背景渐变 DPR≥2 时 1px CSS(2 物理像素)颜色分割 = 1 物理像素;DPR=1 时视觉欺骗 无(DPR≥2 最优) 模拟细线条,无兼容性问题
box-shadow DPR≥2 时,0.5px CSS 偏移 = 1 物理像素阴影 DPR≥2 非边框线条适用

核心一句话:所有 “真实 0.5px 效果”(1 物理像素)都依赖 DPR≥2 的高清屏(利用 CSS 像素与物理像素的映射关系);DPR=1 时只能模拟,无法实现物理级半像素。

以下是包含 CSS 像素 / 物理像素 / DPR 关系说明 的 0.5px 兼容代码合集,每个方法都标注核心逻辑和适用场景,可直接复制使用:

一、说明(所有方法通用)

  • 核心目标:让线条最终占用 1 个物理像素(视觉最细)。
  • 像素关系:1 CSS 像素 = DPR × DPR 物理像素(默认缩放 1 时),高清屏(DPR≥2)需通过代码 “压缩” 映射关系。
  • 适配原则:优先选兼容性广、无布局影响的方法(如 SVG、transform 缩放)。

二、6 种实用兼容代码

1. 推荐首选:transform: scale (0.5) 缩放(DPR≥2 生效,兼容性最好)

  • 核心逻辑:1px CSS 像素(DPR=2 时对应 2 物理像素)→ 缩放 50% → 最终 1 物理像素。
  • 适用场景:边框、独立线条,不影响布局。
/* 通用细线条类(上下左右可按需调整) */
.thin-line {
  position: relative;
  /* 父容器需触发 BFC,避免线条溢出 */
  overflow: hidden;
}

.thin-line::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 1px; /* 1px CSS = 2 物理像素(DPR=2) */
  background: #000; /* 线条颜色 */
  transform: scaleY(0.5); /* 垂直缩放 50% → 2 物理像素 → 1 物理像素 */
  transform-origin: 0 0; /* 缩放原点避免偏移 */
}

/* 横向线条(默认)、纵向线条(按需添加) */
.thin-line-vertical::after {
  width: 1px;
  height: 100%;
  transform: scaleX(0.5);
}
  • 使用方式:<div class="thin-line">内容</div>

2. 跨 DPR 优选:SVG 绘制(所有设备适配,精准无模糊)

  • 核心逻辑:SVG 矢量渲染不依赖像素映射,直接指定 1 物理像素线条(DPR≥2 精准,DPR=1 模拟细线条)。
  • 适用场景:UI 严格还原、跨设备兼容(推荐用于分割线、边框)。
<!-- 横向细线条(直接嵌入,可复用) -->
<svg class="svg-thin-line" width="100%" height="1" xmlns="http://www.w3.org/2000/svg">
  <!-- y1="0.5" + stroke-width="1" → 直接对应 1 物理像素(DPR≥2) -->
  <line x1="0" y1="0.5" x2="100%" y2="0.5" stroke="#000" stroke-width="1" />
</svg>

<!-- 纵向细线条(宽度 100%,高度自适应) -->
<svg class="svg-thin-line-vertical" width="1" height="100%" xmlns="http://www.w3.org/2000/svg">
  <line x1="0.5" y1="0" x2="0.5" y2="100%" stroke="#000" stroke-width="1" />
</svg>

<!-- 样式优化(可选) -->
<style>
  .svg-thin-line {
    display: block;
    margin: 8px 0; /* 上下间距 */
  }
</style>
  • 使用方式:直接嵌入 HTML,修改 stroke 颜色、width/height 适配场景。

3. 现代设备:直接 0.5px 声明(简洁高效,DPR≥2 + 现代浏览器)

  • 核心逻辑:DPR=2 时,0.5px CSS 像素 = 1 物理像素,浏览器直接渲染。
  • 适用场景:iOS 9+、Android 8.0+ 设备,无需兼容旧机型。
/* 直接声明,简洁高效 */
.simple-thin-line {
  border-bottom: 0.5px solid #000; /* 横向线条 */
  /* 纵向线条:border-left: 0.5px solid #000; */
}

/* 兼容写法(部分浏览器需前缀) */
.compact-thin-line {
  border-bottom: 0.5px solid #000;
  -webkit-border-bottom: 0.5px solid #000;
}
  • 使用方式:<div class="simple-thin-line">内容</div>

4. 全局适配:viewport 缩放(DPR≥2,全局细线条统一)

  • 核心逻辑:缩放页面为 1/DPR,让 1px CSS 像素 = 1 物理像素(需配合 rem 布局)。
  • 适用场景:整个页面需要大量细线条,愿意调整布局单位。
<!-- 第一步:设置 viewport(初始缩放 1.0) -->
<meta name="viewport" id="viewport" content="width=device-width, user-scalable=no">

<!-- 第二步:动态调整缩放比例 -->
<script>
  (function() {
    const dpr = window.devicePixelRatio || 1;
    const viewport = document.getElementById('viewport');
    // 缩放 1/DPR,让 1px CSS = 1 物理像素(DPR=2 → 缩放 50%)
    viewport.setAttribute('content', `width=device-width, initial-scale=${1/dpr}, user-scalable=no`);
    
    // 可选:调整 rem 基准值(避免布局错乱)
    const html = document.documentElement;
    html.style.fontSize = `${16 * dpr}px`; // 1rem = 16*dpr px(适配缩放后布局)
  })();
</script>

<!-- 第三步:直接写 1px 即可(此时 1px = 1 物理像素) -->
<style>
  .global-thin-line {
    border-bottom: 1px solid #000; /* 实际是 1 物理像素细线条 */
    margin: 0.5rem 0; /* rem 单位适配缩放后布局 */
  }
</style>
  • 使用方式:全局引入脚本,之后所有 1px 边框都会变成细线条。

5. 视觉模拟:背景渐变(无兼容性问题,DPR≥2 最优)

  • 核心逻辑:1px CSS 容器(DPR=2 时 2 物理像素)→ 颜色分割为 50% 透明 + 50% 有色 → 视觉上 1 物理像素。
  • 适用场景:背景线条、无法用边框 / 伪元素的场景。
/* 横向线条 */
.gradient-thin-line {
  height: 1px;
  width: 100%;
  /* 上半透明,下半有色 → 视觉上细线条 */
  background: linear-gradient(to bottom, transparent 50%, #000 50%);
  background-size: 100% 1px;
}

/* 纵向线条 */
.gradient-thin-line-vertical {
  width: 1px;
  height: 100%;
  background: linear-gradient(to right, transparent 50%, #000 50%);
  background-size: 1px 100%;
}
  • 使用方式:<div class="gradient-thin-line"></div>(独立线条容器)。

6. 非边框场景:box-shadow 模拟(DPR≥2,适合阴影类线条)

  • 核心逻辑:DPR=2 时,0.5px CSS 偏移 = 1 物理像素,阴影即细线条。
  • 适用场景:无需占用布局空间的线条(如文字下方细下划线)。
.shadow-thin-line {
  height: 0;
  /* y 轴偏移 0.5px → 1 物理像素,无模糊、无扩散 */
  box-shadow: 0 0.5px 0 #000;
  -webkit-box-shadow: 0 0.5px 0 #000; /* 兼容 Safari */
}

/* 文字下划线示例 */
.text-thin-underline {
  display: inline-block;
  box-shadow: 0 0.5px 0 #000;
  padding-bottom: 2px;
}
  • 使用方式:<span class="text-thin-underline">带细下划线的文字</span>

三、使用建议

  1. 优先选 transform 缩放 或 SVG 绘制:兼容性广、无布局影响,覆盖 99% 场景。
  2. 现代设备(iOS 9+/Android 8.0+)直接用 0.5px 声明:代码最简洁。
  3. 全局大量细线条用 viewport 缩放:需配合 rem 布局,一次性解决所有线条问题。

HTML&CSS&JS:赛博木鱼

作者 前端Hardy
2025年11月14日 09:55

用 HTML + CSS + JS 打造一个禅意十足的互动小应用——赛博木鱼, 本文将拆解其核心实现逻辑,带你从 0 到 1 理解 “敲木鱼” 背后的技术细节。


大家复制代码时,可能会因格式转换出现错乱,导致样式失效。建议先少量复制代码进行测试,若未能解决问题,私信回复源码两字,我会发送完整的压缩包给你。

演示效果

演示效果

演示效果

HTML&CSS


<!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>
        body {
            margin: 0;
            padding: 0;
            background-color: #121212;
            color: #FFFFFF;
            font-family: 'Kaiti SC', 'PingFang SC', sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            overflow: hidden;
        }

        .title-container {
            text-align: center;
            margin-bottom: 20px;
        }

        .main-title {
            color: #FFFFFF;
            font-size: 6vw;
            font-family: 'Kaiti SC', sans-serif;
            background: linear-gradient(180deg,
                rgba(255, 176, 103, 0.05) 0%,
                rgba(115, 55, 0, 0.02) 100%);
            background-clip: text;
            margin: 0;
            line-height: 1;
        }

        .score-container {
            text-align: center;
            margin: 20px 0;
        }

        .score {
            font-size: 10vh;
            font-family: 'PingFang SC', sans-serif;
            font-weight: 900;
            margin: 0;
        }

        .description {
            font-size: 5vh;
            font-family: 'PingFang SC', sans-serif;
            margin-top: 10px;
        }

        .wooden-fish-container {
            position: relative;
            width: 300px;
            height: 250px;
            display: flex;
            justify-content: center;
            align-items: center;
            margin: 30px 0;
        }

        .wooden-fish {
            width: 284px;
            height: 236px;
            transition: transform 0.1s ease;
        }

        .mallet {
            position: absolute;
            top: 20px;
            right: 20px;
            width: 119px;
            height: 69px;
            transition: transform 0.1s ease;
            z-index: 10;
        }

        .floating-text {
            position: absolute;
            top: -50px;
            left: 50%;
            transform: translateX(-50%);
            font-family: 'PingFang SC', sans-serif;
            font-size: 1.5em;
            color: #FFFFFF;
            opacity: 0;
            pointer-events: none;
            animation: floatUp 0.4s ease-out forwards;
        }

        @keyframes floatUp {
            0% {
                opacity: 1;
                transform: translateX(-50%) translateY(0);
            }
            100% {
                opacity: 0;
                transform: translateX(-50%) translateY(-60px);
            }
        }

        .bottom-text {
            font-family: 'Kaiti SC', sans-serif;
            font-size: 1.2em;
            text-align: center;
            margin-top: 30px;
        }
    </style>
</head>
<body>
    <div class="title-container">
        <div class="main-title">赛博木鱼</div>
    </div>

    <div class="score-container">
        <div class="score" id="score">0</div>
        <div class="description">功德</div>
    </div>

    <div class="wooden-fish-container" id="fishContainer">
        <img src="https://img.alicdn.com/imgextra/i3/O1CN012CO0YU1VSfNs506ZS_!!6000000002652-2-tps-284-236.png"
             alt="木鱼" class="wooden-fish" id="woodenFish">
        <img src="https://img.alicdn.com/imgextra/i1/O1CN01tKb5Et1aSjWRjCHK3_!!6000000003329-2-tps-119-69.png"
             alt="锤子" class="mallet" id="mallet">
    </div>

    <div class="bottom-text">轻敲木鱼,细悟赛博真经。</div>

    <audio id="clickSound" preload="auto">
        <source src="https://qianwen.alicdn.com/resource/qiaomuyu.mp3" type="audio/mpeg">
    </audio>

    <script>
        let score = 0;
        const scoreElement = document.getElementById('score');
        const woodenFish = document.getElementById('woodenFish');
        const mallet = document.getElementById('mallet');
        const fishContainer = document.getElementById('fishContainer');
        const clickSound = document.getElementById('clickSound');

        fishContainer.addEventListener('click', function(e) {
            // 播放音效
            clickSound.currentTime = 0;
            clickSound.play();

            // 锤子旋转动画
            mallet.style.transform = 'rotate(-25deg)';
            setTimeout(() => {
                mallet.style.transform = 'rotate(0deg)';
            }, 100);

            // 木鱼放大动画
            woodenFish.style.transform = 'scale(1.1)';
            setTimeout(() => {
                woodenFish.style.transform = 'scale(1)';
            }, 100);

            // 增加分数
            score++;
            scoreElement.textContent = score;

            // 创建漂浮文字
            const floatingText = document.createElement('div');
            floatingText.className = 'floating-text';
            floatingText.textContent = '功德+1';
            fishContainer.appendChild(floatingText);

            // 移除漂浮文字
            setTimeout(() => {
                floatingText.remove();
            }, 400);
        });
    </script>
</body>
</html>




HTML

  • title-container:标题容器:承载 “赛博木鱼” 主标题,通过居中布局突出视觉核心
  • score-container:分数展示区:包含 “功德数” 和 “功德” 描述,是用户交互的核心反馈载体
  • wooden-fish-container:交互核心容器
  • wooden-fish:木鱼图片:通过 CDN 引入资源,无需本地存储,是点击交互的视觉核心
  • mallet:锤子图片:绝对定位在木鱼右上方,点击时通过旋转动画模拟 “敲击” 动作
  • clickSound:音效载体:预加载木鱼敲击音效(preload="auto"),点击时触发播放,增强沉浸感
  • floating-text:动态生成元素:点击时创建 “功德 + 1” 漂浮文字,完成交互反馈闭环

CSS

  • .mallet:点击时锤子绕右上角旋转 25 度,模拟 “敲下去” 的动作,100 毫秒快速回弹
  • .wooden-fish:点击时木鱼轻微放大 1.1 倍,模拟 “被敲击后的震动”,与锤子动作同步:
  • .floating-text:自动向上淡出,营造“功德升天”的视觉效果。

JavaScript

  1. 初始化变量:获取 DOM 元素与状态
let score = 0; // 功德数初始值
const scoreElement = document.getElementById('score'); // 分数显示元素
const woodenFish = document.getElementById('woodenFish'); // 木鱼元素
const mallet = document.getElementById('mallet'); // 锤子元素
const fishContainer = document.getElementById('fishContainer'); // 交互容器
const clickSound = document.getElementById('clickSound'); // 音效元素

通过 getElementById 获取核心交互元素,提前定义功德数状态。

  1. 核心交互:点击事件处理
fishContainer.addEventListener('click', function(e) {
    // 1. 播放敲击音效(每次点击从头播放,支持连续敲击)
    clickSound.currentTime = 0;
    clickSound.play();

    // 2. 锤子旋转动画:敲下→回弹
    mallet.style.transform = 'rotate(-25deg)';
    setTimeout(() => {
        mallet.style.transform = 'rotate(0deg)';
    }, 100);

    // 3. 木鱼缩放动画:放大→还原
    woodenFish.style.transform = 'scale(1.1)';
    setTimeout(() => {
        woodenFish.style.transform = 'scale(1)';
    }, 100);

    // 4. 功德数累加与更新
    score++;
    scoreElement.textContent = score;

    // 5. 生成“功德+1”漂浮文字
    const floatingText = document.createElement('div');
    floatingText.className = 'floating-text';
    floatingText.textContent = '功德+1';
    fishContainer.appendChild(floatingText);

    // 6. 动画结束后移除文字(避免DOM堆积)
    setTimeout(() => {
        floatingText.remove();
    }, 400);
});
  1. 关键技术点拆解

音效重置:clickSound.currentTime = 0 确保连续点击时音效不叠加,每次都是完整的 “敲击声”;

动画同步:锤子旋转和木鱼缩放的过渡时长(0.1s)与定时器延迟(100ms)一致,动作协调;

DOM 优化:漂浮文字动画结束后通过 remove()移除,避免页面 DOM 元素过多导致性能问题;

无依赖设计:不使用任何框架,仅原生 API,兼容性覆盖所有现代浏览器。


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

❌
❌