卡片式布局现在是前端日常开发里绕不开的基本功不管是后台管理系统的数据看板还是移动端的信息流页面甚至个人博客的文章列表拆开来看都是一张张卡片。很多初学者能写出“看着像卡片”的界面——有背景色、有圆角、有阴影但只要页面稍微一变复杂就会出现盒子尺寸对不齐、阴影被裁掉、间距忽大忽小这类问题。原因无非两个盒模型没有彻底理解间距和阴影的设置又全凭感觉。这篇内容不打算讲花哨的动效也不碰复杂的布局框架就聚焦在最核心的一层用纯 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-columns和gap写起来更简洁、控制力也更强。你还需要注意的是卡片本身的 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 classcard-list article classcard div classcard__cover img srcproduct-1.jpg alt商品图片 / /div div classcard__body h3 classcard__title商品名称/h3 p classcard__desc商品描述文字简要介绍该商品的核心卖点。/p div classcard__footer span classcard__price¥199/span button classcard__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-fill和auto-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__footer用margin-top: auto;把底部区域推到底部。这里有一个之前操作里常见的困惑为什么已经在 column flex 容器中footer 还是不能自动到最底因为 flex 容器内元素默认是按内容高度排列的margin-top: auto会吞噬所有剩余空间把 footer 推到容器底部。这也是很多“卡片高低不平”场景的标准解法。4.3 响应式改造思路上面的代码在移动端只需要微调。grid 的auto-fitminmax已经能让卡片在窄屏下自动变成一列所以媒体查询里主要调整间距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 属性导致产生了新的渲染上下文。排查顺序我一般是这样的先看父容器有没有overflow: hidden、overflow-x: scroll、over-flow-y: auto这类属性。再看父容器有没有contain、filter、opacity: 小于1、transform这类会创建包含块的属性。最后看卡片的阴影方向是不是溢出到了视口之外或者设置了负值的 spread。如果确认是父容器 overflow: hidden 导致的问题有两种处理方式把 overflow 从父容器上移除或者把阴影从box-shadow改到伪元素上让伪元素作为卡片子元素参与布局但仍然会被 overflow 裁剪所以不行的话就得把阴影真正“溢出”出去。这种情况下最稳妥的方案是给卡片本身加一层等大的父级 wrapperwrapper 不带 overflow卡片放在 wrapper 里面阴影自然可视范围内。5.2 多张卡片高度无法对齐卡片式布局里最常见的视觉问题是同一行卡片高度不一致有的长有的短底部参差不齐。如果用的是 flex 布局容器保持display: flex; flex-wrap: wrap;默认情况下同一行的项目高度是按内容最高的那个拉伸的所以理论上高度会自动对齐。但你会发现卡片的背景色确实拉伸到了同一高度内部的内容却没有填满底部留了一截空白。这时候需要给卡片内部的内容区域设置flex: 1;让内容区吸收剩余高度。如果高度真的没有被拉伸先检查容器上是不是不小心设置了align-items: flex-start;这个属性会把项目高度重置为内容高度禁用掉默认的 stretch 行为。还有一种情况是把flex-direction: column和align-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。这个习惯在项目规模变大之后省下来的是我在全局搜索和改样式上反复浪费的时间也基本杜绝了“为什么这张卡片阴影重、那张阴影轻”这类视觉不一致的问题。