移动端卡片边框怎么做高级?我用 CSS 实现了设计师的刁钻要求
一个让产品经理和设计师都满意的卡片边框方案
📖 前言
上周设计突然甩过来一张图,问我能不能不切图做出这种效果?
我蒙了一下第一反应感觉可以,无非就是常规的伪类+渐变。但尝试了一下发现两个致命问题:
1、border-image支持渐变但不支持每条边自定义设置;
2、使用伪类可以解决线的问题但是不能解决圆角问题;
忙乎半天又问了问ai感觉还是实现起来不容易,但随后产品过来又是那老一套。拿着别人家的产品看人家这个如何好看,如何优雅,巴拉巴拉。大有一种:
别人能做,你做不了。
这是不能接受的,于是又潜心研究了下,有了最后的效果。
🎯 需求拆解
先梳理一下具体需求:
| 需求 | 描述 |
|---|---|
| 位置 | 卡片左下角 L 形(左边 + 底边) |
| 渐变 | 左下角颜色最深,向两端渐淡 |
| 粗细 | 视觉上 1px |
| 长度 | 左边和底边长度大致相等 |
| 圆角 | 适配卡片 20px 圆角 |
| 性能 | 纯 CSS,无图片,无 SVG |
看起来简单,做起来全是坑。
🧪 方案探索
方案一:两个伪元素分别画线
最直观的想法:用 ::before 画底部线,::after 画左边线。
scss
.card {
&::before {
// 底部线
background: linear-gradient(90deg, gold, transparent);
}
&::after {
// 左边线
background: linear-gradient(0deg, gold, transparent);
}
}
问题:两条线在圆角处有接缝,怎么都对不齐。调整了半天,还是能看到明显的拼接痕迹。
结论:放弃,圆角处无法完美衔接。
方案二:SVG 路径描边
SVG 可以精确控制路径和圆角,效果确实完美。
问题:
- 需要额外 HTML 结构
- 移动端多一个网络请求或内联代码
- 响应式适配需要额外处理
结论:能用,但不够优雅,性能也不够极致。
方案三:border-image + 渐变
scss
border-image: radial-gradient(circle at bottom left, gold, transparent) 1;
问题:border-image 会覆盖四边,无法只控制左下角。
结论:放弃。
方案四:radial-gradient + mask(最终方案)
经过多次尝试,我发现径向渐变的圆心在左下角时,渐变会自然地向左和向上扩散,形成完美的 L 形。
配合 mask 组合,可以精确控制只显示边框区域,而不是整个渐变圆。
完美解决所有问题!
💻 最终代码
以下是基于vue2的一个组件CornerGradientCard,开箱即用。
但注意基于他的点击事件要使用click.native!!!
<template>
<div
:class="`gradient-wrapper ${type} `"
:style="wrapperStyle"
>
<div
class="gradient-wrapper__content"
:style="{ borderRadius: radiusRem }"
>
<slot></slot>
</div>
</div>
</template>
<script>
/** 与 postcss.config.js 中非 vant 资源的 rootValue(75) 一致,设计稿 px → rem */
const POSTCSS_ROOT_VALUE = 75
function pxToRem(px) {
const n = Number(px)
if (Number.isNaN(n)) return '0rem'
return `${parseFloat((n / POSTCSS_ROOT_VALUE).toFixed(10))}rem`
}
export default {
props: {
type: {
default: '',
type: String
},
radius: {
default: 12,
type: Number
},
marginBottom: {
default: 14,
type: Number
}
},
computed: {
radiusRem() {
return pxToRem(this.radius)
},
wrapperStyle() {
const r = this.radiusRem
return {
borderRadius: r,
marginBottom: pxToRem(this.marginBottom),
'--corner-radius': r
}
}
}
}
</script>
<style lang="scss" scoped>
$gradient-first-percent: 4%; // 第一个实色节点百分比
$gradient-second-percent: 10%; // 第二个半透明节点百分比
$gradient-transparent-percent: 30%; // 透明节点百分比
.gradient-wrapper {
width: 100%;
position: relative;
padding: 0 0 1px 1px;
box-sizing: border-box;
background-color: #fff;
overflow: hidden;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
&__content {
width: 100%;
position: relative;
z-index: 3;
margin: 0 0 1px 1px;
box-sizing: border-box;
overflow: hidden;
background-color: transparent;
}
// 渐变边框线(核心)
&::after {
content: '';
position: absolute;
z-index: 2;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: var(--corner-radius);
pointer-events: none;
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask-composite: exclude;
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
padding: 1px;
}
// 渐变底色(光晕效果)
&::before {
content: '';
position: absolute;
z-index: 1;
bottom: 1px;
left: 1px;
width: 143px;
height: 73px;
border-radius: var(--corner-radius);
filter: blur(10px);
pointer-events: none;
}
&.WX {
&::after {
background: radial-gradient(
circle at bottom left,
#B6E2C8 $gradient-first-percent,
#DFF7EA $gradient-second-percent,
transparent $gradient-transparent-percent
);
}
&::before {
background: radial-gradient( 83% 83% at 31% 52%, #F0FBF5 0%, rgba(239,255,246,0) 100%);
}
}
}
</style>
<CornerGradientCard
v-for="(item, index) in infoData"
:key="item.id"
:id="item.id"
:type="item.type"
@click.native="clickItem(item)"
>
<!-- 卡片内容 -->
</CornerGradientCard>
🎨 参数调节指南
| 参数 | 位置 | 作用 | 移动端建议 |
|---|---|---|---|
padding: 1px | .wrapper | 边框粗细 | 保持 1px |
4% / 10% / 30% | 径向渐变 | 边框长度 | 根据卡片大小调整 |
blur(10px) | 光晕 | 柔和度 | 移动端 8-12px 较佳 |
border-radius | 全局 | 圆角 | 与设计稿保持一致 |
当然,基于此样式还可以可发出各种变种,例如将渐变等放到常规的右上角,替代常规的卡片标签展示样式。
如果这篇文章对你有帮助,烦请动动发财的小手点个赞~