纯CSS卡片式布局:从盒模型到阴影间距的实战指南
2026/9/7 19:15:31 网站建设 项目流程

卡片式布局现在是前端日常开发里绕不开的基本功,不管是后台管理系统的数据看板,还是移动端的信息流页面,甚至个人博客的文章列表,拆开来看都是一张张卡片。很多初学者能写出“看着像卡片”的界面——有背景色、有圆角、有阴影,但只要页面稍微一变复杂,就会出现盒子尺寸对不齐、阴影被裁掉、间距忽大忽小这类问题。原因无非两个:盒模型没有彻底理解,间距和阴影的设置又全凭感觉。

这篇内容不打算讲花哨的动效,也不碰复杂的布局框架,就聚焦在最核心的一层:用纯 CSS 把卡片式布局做扎实。从盒模型原理讲到阴影参数怎么调,从间距用 margin 还是 padding 讲到实际项目里的踩坑记录,最后带一个完整的页面实例。适合正在学 CSS 布局、或者写了好一阵子前端但总觉得布局不够稳的同学,看完可以直接照着写,也能帮你们排查掉大部分“看着没问题但就是不对”的疑难杂症。

1. 卡片式布局的底层原理:先弄清楚盒子是怎么画的

1.1 所有视觉效果的根源都在盒模型

很多人在写卡片时,心里想的是“一个圆角矩形放在页面上”,写出来的 CSS 也就是 background-color、border-radius、box-shadow 三件套。这样写确实能出效果,但一旦牵涉到卡片内部再嵌套内容、多个卡片放在同一排、卡片高度要一致这类需求,问题就全冒出来了。

卡片的本质是“盒子”,而盒模型规定了浏览器如何计算这个盒子的尺寸和空间占用。每一个元素在页面上都会被浏览器渲染成一个矩形区域,这个区域从内到外依次是内容区(content)、内边距(padding)、边框(border)、外边距(margin)。不同盒模型下,元素的 width 和 height 含义是完全不同的。

举个例子,你给卡片设置了width: 300px;,如果盒模型是默认的 content-box,那么这 300px 只是内容区的宽度。一旦加上左右各 20px 的 padding、再加 1px 的边框,浏览器实际渲染出来的总宽度就变成了 300 + 202 + 12 = 342px。这在单卡片场景下不明显,但多张卡片并排布局时,342px 和 300px 混在一起,网格就会错位,怎么调都差那么几像素。

这就是盒模型的第一道坎:你写的 width 不一定等于盒子实际占用的宽度

1.2 为什么 border-box 是更符合直觉的选择

解决上面那个问题最简单的办法就是从源头统一盒模型。全局设置box-sizing: border-box;后,你写的 width 就是包含 padding 和 border 在内的总宽度。比如还是width: 300px;,加了 20px 的 padding 和 1px 的边框后,内容区会自动压缩到 300 - 202 - 12 = 258px,盒子整体宽度稳定在 300px。

个人建议所有项目都在样式表的最前面加上这一行:

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

这样做的收益是:布局时的宽度规划和视觉上看到的效果是统一的。你在设计稿里量到一个卡片占 300px 宽,写 300px 就是 300px,不用每次都在心里计算 padding 会不会把卡片“撑爆”。

当然,有人会问:这会不会影响某些依赖默认 content-box 行为的组件?对于第三方组件库来说,确实偶尔会遇到,但现在主流框架和组件库基本都已经兼容了全局 border-box,实际开发中直接全局设置问题不大。如果你用的是旧版组件库,可以考虑只对项目自己的业务样式统一设置,保留组件库自身的盒模型。

1.3 卡片在流式布局中的定位方式

搞清楚了盒子怎么计算尺寸,接下来要解决的是盒子放在哪里。卡片式布局最常见的场景是“一堆卡片排成多行多列”,这时候用旧式的 float 或者手动计算百分比虽然也能做,但都不是最优解。

我的做法是默认使用 flex 或 grid。flex 适合单方向排列的场景,比如一排卡片水平居中对齐、间距均匀;grid 则适合同时控制行和列的场景,比如三列布局、每行卡片高度自动撑开、列间距和行间距分别控制。

选择 flex 还是 grid,核心看你的结构是“一维”还是“二维”。如果所有卡片都在同一行内换行排列,flex 加上flex-wrap: wrap;就够了;如果卡片是规整的行列网格,grid 的grid-template-columnsgap写起来更简洁、控制力也更强。

你还需要注意的是卡片本身的 display 属性。块级元素默认占满整行,多个卡片用 flex 或 grid 容器包住后,它们会自动变成 flex item 或 grid item,原有的块级特性被容器接管,所以常见做法是卡片本身保持块级,不要轻易给它加display: inline-block——这会在卡片之间引入空白字符间隙,反而增加布局负担。

2. 让卡片“立体”起来:阴影和圆角的实战解析

2.1 box-shadow 的每个参数到底在控制什么

阴影是卡片区分于普通色块的关键视觉手段。一个设计走心的卡片,往往不是它颜色多好看,而是阴影给得恰到好处——轻了没有层次,重了显脏。

box-shadow 的基本语法是:

box-shadow: offset-x offset-y blur spread color;

逐项拆解一下:

  • offset-x:阴影的水平偏移,正数向右,负数向左,平时我用 0 居多。
  • offset-y:垂直偏移,正数向下,负数向上。这是最影响真实感的参数,模拟光源从上方照下来时,卡片下方应该有阴影,所以通常取 2px 到 8px 左右的正值。
  • blur(模糊半径):阴影边缘的柔和程度,值越大扩散越广、边界越模糊。它决定阴影是“硬边”还是“软边”。卡片阴影一般不小于 4px,太小会显得像描边。
  • spread(扩散半径):阴影在模糊之前的扩展范围,正数让阴影变大变实,负数让阴影收拢。这个参数日常用得少,做按压效果时才偶尔用。
  • color:阴影颜色,纯黑往往太重,我一般用带透明度的颜色,比如rgba(0, 0, 0, 0.08)这类。

给一个最常见的卡片阴影示例:

.card { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); }

这行代码在大多数白底界面下都会有不错的质感。阴影不抢眼,但能明显看出卡片浮在背景之上。

2.2 悬浮感和层次感是怎么调出来的

比单层阴影更进一步的是“悬浮感”。你可以仔细观察一些设计精细的网站,卡片在正常状态下阴影比较收敛,但鼠标悬停时阴影会明显变大变柔,卡片像是被“抬起来”了。实现上就是 transition 配合 hover 动态修改 shadow。

我常用的悬浮阴影方案是基于两层阴影叠加的:

.card { transition: box-shadow 0.2s ease; } .card:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), 0 12px 32px rgba(0, 0, 0, 0.12); }

第一层阴影贴近卡片,提供明确的边界感;第二层阴影距离更远、模糊更大,模拟环境光的漫反射。两层叠加后,卡片的漂浮感会比单层阴影提升一个档次。

这里有个小技巧:当阴影扩散范围较大时,卡片边缘和阴影之间会有一小段“断档”,看起来像卡片没有真正压在阴影上。解决办法就是把 hover 状态的 offset-y 也跟着加大或者微调过渡曲线,让阴影移动的方向和幅度更自然。比如从0 2px 8px过渡到0 8px 24px,而不是只改 blur 和颜色。

2.3 圆角、边框与背景的配合细节

box-shadow、border-radius、background-color 这三者共同决定了一张卡片的基础外观。其中最容易出问题的是圆角与背景色的关系。

当卡片设置border-radius: 12px;时,background-color 会被自动裁剪到圆角范围内,这没问题。但如果你还在卡片内部塞了一个带背景色的子元素(比如封面图、色块头部),默认情况下这个子元素并不会继承父级的圆角裁剪,它的直角就会“戳破”卡片的圆角边缘,露出一个很刺眼的直角。

解决思路是给子元素也加圆角,或者直接给父卡片加overflow: hidden;。前者精确但麻烦,需要手动画:左上角、右上角对应卡片头部元素的border-radius: 12px 12px 0 0;;后者粗暴但省事,一次性把里面所有内容都裁剪到卡片圆角范围内。要注意 overflow: hidden 的副作用是它会同时裁掉 drop-shadow(用 filter 做的外阴影不受影响),所以如果你卡片的阴影是借助伪元素或子元素溢出来实现的,这个方法会导致阴影被裁没,需要取舍。

边框和背景的关系也有坑。设置了 1px 的边框,然后边框颜色和背景色接近,视觉上确实只出现了淡淡的描边,但盒模型计算宽度时会比不设边框多出 2px。在全局 border-box 的设置下,这不会影响卡片总宽度,但会影响内部内容区的可用宽度。设计稿里的内容排布如果非常紧凑,1px 的差距也会让文字折行。我的习惯是,如果只是为了描边效果,优先使用 border 之外的方式实现,比如加一圈 inner shadow 模拟描边,或者用一个 inset 的 box-shadow:

.card { box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06); }

这种“假边框”好处是不占布局空间,颜色和透明度可以调得很细腻,适合用在卡片和背景色接近、但需要轻微区分边界的场景。

3. 间距是卡片的呼吸感:margin、padding、gap 的取舍

3.1 内外间距各有各的职责,别混用

在卡片布局里,间距分两个层次:一是卡片与卡片之间的间距,二是卡片内部元素与边缘之间的间距。前者用 margin 或者容器 gap,后者用 padding。

有个很常见的误区是:为了省事,直接在卡片本身上面加 margin 来拉开卡片间距,再把同一组间距用在内部的内容元素上。结果就是卡片上下左右边缘的距离和卡片内部文字到边缘的距离一模一样,页面看起来像是一格一格的盒子,没有主次和呼吸感。

我建议内外间距分开规划。外层间距的职责是“划清卡片之间的边界”,数值可以相对紧凑,比如 16px 或 20px;内层 padding 的职责是“让内容不顶到边缘”,数值通常比外层大一些,比如 20px 到 24px。这样卡片内部舒适,外部密集但不拥挤。

margin 还有个特性需要特别注意:纵向相邻元素的 margin 会发生折叠。两个上下排列的卡片,下面的卡片设置margin-top: 20px;,上面的卡片设置margin-bottom: 20px;,最终间距是 20px 而不是 40px,因为取的是两者较大的那个值。这个特性在实际项目里经常让人摸不着头脑,明明两边都设置了边距,为什么间距没有相加?所以我现在做卡片列表布局时,优先用 flex/grid 容器的 gap,彻底避开 margin 折叠问题。

3.2 flex 和 grid 的 gap 是布局救星

gap 属性是 CSS 这几年布局体验提升最明显的点。它直接设置在容器上,容器内部的项目之间会自动留出指定的空隙,不用再给每个子元素单独写 margin,也不用处理“最后一个元素的右边距要不要去掉”这种烦人问题。

示例:

.card-list { display: flex; flex-wrap: wrap; gap: 20px; } .card-list { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px 24px; /* 行间距 20px,列间距 24px */ }

用 gap 之后还有一个隐藏收益:间距变得“声明式”了,不会因为 DOM 结构的变化而意外改变。比如你在某两张卡片中间插入一张新卡片,gap 方案下间距自动保持,margin 方案下新加卡片如果没写 margin,间距立刻乱掉。

3.3 卡片内部间距的推荐规范

卡片内部的 padding 取决于内容密度。信息密集型卡片(比如后台列表卡片)我习惯用padding: 16px 20px;,上下紧凑、左右舒展;内容型卡片(比如文章卡片)用padding: 24px;,给标题和摘要足够的留白。

还有一个细节:如果卡片里有“头部标题 + 下方正文”的结构,我通常会单独定义内容区之间的 margin-bottom,而不是统一使用一个公共 class。比如:

.card__title { margin-bottom: 12px; } .card__desc { margin-bottom: 16px; }

这样每个层级的间距都是根据内容的视觉权重单独调的,而不是一刀切。标题和摘要之间的间距略小,摘要和底部操作按钮之间的间距略大,阅读节奏会更舒服。这个经验适合在做信息层级较多的卡片时参考。

4. 完整实操:从零构建一个卡片列表页面

4.1 页面结构与设计目标

这一节我带大家完整做一个项目:一个商品卡片的响应式列表页面。目标是一个良好的卡片式布局应该具备的完整能力——多列网格、卡片内部结构清晰、间距一致、阴影自然、并且在不同屏幕宽度下自适应。

先确立页面结构:

<div class="card-list"> <article class="card"> <div class="card__cover"> <img src="product-1.jpg" alt="商品图片" /> </div> <div class="card__body"> <h3 class="card__title">商品名称</h3> <p class="card__desc">商品描述文字,简要介绍该商品的核心卖点。</p> <div class="card__footer"> <span class="card__price">¥199</span> <button class="card__btn">加入购物车</button> </div> </div> </article> <!-- 重复多个 card --> </div>

结构上要遵循“容器负责布局,卡片负责自身外观,内部元素负责内容”的三层分工。.card-list是容器,管理卡片排列和间距;.card是单张卡片,管理背景、圆角、阴影和内部 padding;.card__body和里面的内容元素各自负责内容排版。

4.2 完整 CSS 实现与关键点讲解

先上完整的 CSS,再逐段拆解:

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } .card-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 24px; max-width: 1200px; margin: 0 auto; padding: 24px; } .card { display: flex; flex-direction: column; background-color: #fff; border-radius: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); transition: box-shadow 0.2s ease, transform 0.2s ease; overflow: hidden; } .card:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), 0 12px 32px rgba(0, 0, 0, 0.12); transform: translateY(-2px); } .card__cover { width: 100%; aspect-ratio: 4 / 3; background-color: #f2f2f2; } .card__cover img { width: 100%; height: 100%; object-fit: cover; } .card__body { display: flex; flex-direction: column; flex: 1; padding: 20px; } .card__title { font-size: 18px; font-weight: 600; margin-bottom: 8px; } .card__desc { font-size: 14px; color: #666; line-height: 1.6; margin-bottom: 16px; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; } .card__footer { display: flex; align-items: center; justify-content: space-between; margin-top: auto; } .card__price { font-size: 20px; font-weight: 700; color: #e95b4a; } .card__btn { padding: 8px 16px; border: none; border-radius: 6px; background-color: #3b82f6; color: #fff; font-size: 14px; cursor: pointer; } @media (max-width: 768px) { .card-list { gap: 16px; padding: 16px; } }

逐段讲解几个关键点。

盒模型重置:第一段代码把 border-box 推广到所有元素,同时重置 margin 和 padding。这是很多前端项目的标准开头,目的是消除浏览器默认样式带来的不确定因素。margin: 0; padding: 0;不是必须的,但做卡片类页面时,重置掉默认的 body 内外边距和标题默认边距,可以避免一些莫名奇妙的间距问题。

grid 网格设置:repeat(auto-fill, minmax(260px, 1fr))是这段布局的核心。它的含义是:每一列的最小宽度是 260px,最大是 1fr(等分剩余空间),浏览器会自动计算能在当前宽度下放下多少列。这样写的好处是,不用为每个断点手工写列数,卡片自动换行,宽度自适应。

auto-fillauto-fit的区别值得单独提一下。auto-fill会保持空列占位,即列轨道数量固定;auto-fit会收拢空列,让有内容的卡片尽量拉伸。对于卡片列表这种场景,我建议用auto-fit——如果只有两张卡片,auto-fit会让它们平均占据整行,看起来更自然,auto-fill则会在右侧留下空白。可以把上面的auto-fill换成auto-fit看效果差异。

卡片高度对齐:.card设置了display: flex; flex-direction: column;.card__body设置了flex: 1;,这样即使卡片高度因为标题行数不同而参差不齐,底部按钮区域也能对齐到卡片底部,不会有的按钮悬在半空、有的卡片底部露白。

封面图比例:用aspect-ratio: 4 / 3;固定封面区域比例,图片用object-fit: cover;覆盖填充。这个组合的好处是无论图片原始尺寸如何,封面区域都能保持统一比例,不会把卡片撑得高矮不一。

文字截断:.card__desc里的-webkit-line-clamp: 2是限制摘要最多显示两行,超出部分用省略号隐藏。这是信息流卡片的标准处理方式,避免每张卡片文字数量不同导致高度差异过大。注意这段语法是基于老版本的 WebKit 特性,现代浏览器兼容性已经很高,可以直接使用。

强制底部对齐:.card__footermargin-top: auto;把底部区域推到底部。这里有一个之前操作里常见的困惑:为什么已经在 column flex 容器中,footer 还是不能自动到最底?因为 flex 容器内元素默认是按内容高度排列的,margin-top: auto会吞噬所有剩余空间,把 footer 推到容器底部。这也是很多“卡片高低不平”场景的标准解法。

4.3 响应式改造思路

上面的代码在移动端只需要微调。grid 的auto-fit+minmax已经能让卡片在窄屏下自动变成一列,所以媒体查询里主要调整间距:

@media (max-width: 480px) { .card-list { grid-template-columns: 1fr; gap: 16px; padding: 12px; } }

如果你想让卡片在小屏下不变成单列而是两列,可以把minmax(260px, 1fr)改成minmax(140px, 1fr),这样窄屏会自动排成两列,但标题和价格文字需要相应缩小编号。移动端卡片式布局的主要权衡是“信息密度”和“阅读舒适度”的取舍。

另外还有一个移动端细节:box-shadow在小屏上可以适当减小,比如从0 2px 8px改成0 1px 4px,因为在手机上阴影过大容易显得页面黑乎乎一片,视觉上比较脏。这个改不影响布局,但会影响观感,建议做响应式时一并处理。

5. 常见问题与排查技巧实录

5.1 阴影被容器裁切

这是 shadow 使用中最容易遇到的问题。你给卡片设置了box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);,但卡片显示出来之后,阴影的某个方向边缘像是被一刀切掉了,只露出模糊影子的内部。

引起裁切的常见原因是:父容器上设置了overflow: hidden;,或者父容器本身有背景色且没把卡片完全包裹住,又或者是父容器使用了 transform 属性导致产生了新的渲染上下文。

排查顺序我一般是这样的:

  1. 先看父容器有没有overflow: hiddenoverflow-x: scrollover-flow-y: auto这类属性。
  2. 再看父容器有没有containfilteropacity: 小于1transform这类会创建包含块的属性。
  3. 最后看卡片的阴影方向是不是溢出到了视口之外或者设置了负值的 spread。

如果确认是父容器 overflow: hidden 导致的问题,有两种处理方式:把 overflow 从父容器上移除,或者把阴影从box-shadow改到伪元素上,让伪元素作为卡片子元素参与布局,但仍然会被 overflow 裁剪,所以不行的话就得把阴影真正“溢出”出去。这种情况下最稳妥的方案是给卡片本身加一层等大的父级 wrapper,wrapper 不带 overflow,卡片放在 wrapper 里面,阴影自然可视范围内。

5.2 多张卡片高度无法对齐

卡片式布局里最常见的视觉问题是:同一行卡片高度不一致,有的长有的短,底部参差不齐。

如果用的是 flex 布局,容器保持display: flex; flex-wrap: wrap;,默认情况下同一行的项目高度是按内容最高的那个拉伸的,所以理论上高度会自动对齐。但你会发现,卡片的背景色确实拉伸到了同一高度,内部的内容却没有填满,底部留了一截空白。这时候需要给卡片内部的内容区域设置flex: 1;,让内容区吸收剩余高度。

如果高度真的没有被拉伸,先检查容器上是不是不小心设置了align-items: flex-start;,这个属性会把项目高度重置为内容高度,禁用掉默认的 stretch 行为。还有一种情况是把flex-direction: columnalign-items: stretch混用,导致横纵方向交互出了问题。

排查思路可以用浏览器开发者工具的 Layout 面板,切换查看每个盒子的尺寸。重点看两个值:一是每个 card 的实际高度是否一致,二是 card 内部各个子元素的高度分布。

5.3 间距出现多 4px 或 8px 的偏差

页面上的间距总是比设计稿多出奇怪的几像素,排查时先检查浏览器默认样式。比如 design 稿里卡片间距是 20px,你设置了gap: 20px,视觉上却像 24px 或者更多。原因往往是卡片本身还带有默认的 margin,而 grid 或 flex 的 gap 不会吞掉项目自身的 margin,两者会叠加。

解决办法就是全局重置或者精确覆盖。推荐使用第一节里的全局统一重置策略,之后间隙值用浏览器计算面板去核对,发现自己写的是 20px,浏览器算出来是 24px,就去找哪个元素带默认 margin。

还有一个容易被忽视的坑是图片元素。img 默认不是块级,会有几个像素的 baseline 空隙,导致图片底部和卡片内容区之间多出几像素。处理方式是给所有img加上display: block;,或者让父容器设置font-size: 0,推荐前者。

5.4 阴影和动画的性能注意事项

阴影不是不能多用,但要清楚它的渲染成本。box-shadow 在每次绘制时都会产生一次额外的模糊运算,当页面有大量卡片同时渲染,或者 hover 状态频繁触发动画时,性能差的设备上会出现卡顿。

优化手段有以下几种:

  • 尽量使用 transform 和 opacity 来触发动画,而不是直接动画 box-shadow,因为 transform 可以由 GPU 加速,而 box-shadow 的变化会强制浏览器重新执行绘制流程。
  • hover 阴影变化时,可以保留一层静态阴影,只改变其透明度和位移,这样变化代价更小。
  • 卡片数量特别多(比如虚拟列表中的几百张卡)时,考虑减少阴影层数,或者用伪元素 + filter 的方式替代,但这个方案本身也有性能消耗,需要实际测试。
  • 画圆角较大的卡片时,阴影和 border-radius 的组合在某些浏览器上会出现锯齿残留,如果放大看有毛边,说明这张卡的阴影模糊半径和圆角半径的比值不太合适,微调一下 blur 值或者 spread 值往往能消失。

最后分享一个我自己的习惯:所有卡片的阴影尽量统一管理,可以用 CSS 变量维护一个阴影变量表:

:root { --shadow-card: 0 2px 8px rgba(0, 0, 0, 0.08); --shadow-card-hover: 0 4px 12px rgba(0, 0, 0, 0.08), 0 12px 32px rgba(0, 0, 0, 0.12); }

这样全站卡片的阴影风格保持一致,后续要统一调整也只需要改一个地方,不用满项目跑着找 box-shadow。这个习惯在项目规模变大之后省下来的是我在全局搜索和改样式上反复浪费的时间,也基本杜绝了“为什么这张卡片阴影重、那张阴影轻”这类视觉不一致的问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询