移动端卡片边框怎么做高级?我用 CSS 实现了设计师的刁钻要求

移动端卡片边框怎么做高级?我用 CSS 实现了设计师的刁钻要求

一个让产品经理和设计师都满意的卡片边框方案

📖 前言

上周设计突然甩过来一张图,问我能不能不切图做出这种效果?

我蒙了一下第一反应感觉可以,无非就是常规的伪类+渐变。但尝试了一下发现两个致命问题:

1、border-image支持渐变但不支持每条边自定义设置;

2、使用伪类可以解决线的问题但是不能解决圆角问题;

忙乎半天又问了问ai感觉还是实现起来不容易,但随后产品过来又是那老一套。拿着别人家的产品看人家这个如何好看,如何优雅,巴拉巴拉。大有一种:

别人能做,你做不了。

这是不能接受的,于是又潜心研究了下,有了最后的效果。

🎯 需求拆解

先梳理一下具体需求:

需求 描述
位置 卡片左下角 L 形(左边 + 底边)
渐变 左下角颜色最深,向两端渐淡
粗细 视觉上 1px
长度 左边和底边长度大致相等
圆角 适配卡片 20px 圆角
性能 纯 CSS,无图片,无 SVG

看起来简单,做起来全是坑。

🧪 方案探索

方案一:两个伪元素分别画线

最直观的想法:用 ::before 画底部线,::after 画左边线。

scss

css 复制代码
.card {
  &::before {
    // 底部线
    background: linear-gradient(90deg, gold, transparent);
  }
  &::after {
    // 左边线
    background: linear-gradient(0deg, gold, transparent);
  }
}

问题:两条线在圆角处有接缝,怎么都对不齐。调整了半天,还是能看到明显的拼接痕迹。

结论:放弃,圆角处无法完美衔接。


方案二:SVG 路径描边

SVG 可以精确控制路径和圆角,效果确实完美。

问题

  • 需要额外 HTML 结构
  • 移动端多一个网络请求或内联代码
  • 响应式适配需要额外处理

结论:能用,但不够优雅,性能也不够极致。


方案三:border-image + 渐变

scss

css 复制代码
border-image: radial-gradient(circle at bottom left, gold, transparent) 1;

问题border-image 会覆盖四边,无法只控制左下角。

结论:放弃。


方案四:radial-gradient + mask(最终方案)

经过多次尝试,我发现径向渐变的圆心在左下角时,渐变会自然地向左和向上扩散,形成完美的 L 形。

配合 mask 组合,可以精确控制只显示边框区域,而不是整个渐变圆。

完美解决所有问题!

💻 最终代码

以下是基于vue2的一个组件CornerGradientCard,开箱即用。

但注意基于他的点击事件要使用click.native!!!

vue 复制代码
<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>
vue 复制代码
 <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 全局 圆角 与设计稿保持一致

当然,基于此样式还可以可发出各种变种,例如将渐变等放到常规的右上角,替代常规的卡片标签展示样式。

如果这篇文章对你有帮助,烦请动动发财的小手点个赞~

相关推荐
计算机魔术师3 分钟前
实测GPT-6 Astra后,我发现过去十年的专业门槛正在失效
前端
虎头金猫6 分钟前
Pic Smaller 本地部署:压缩图片、转换格式,再搭建自己的 Web 工具
运维·前端·开源·aigc·ai编程·ai写作
hunterandroid6 分钟前
Android ANR 排查实战:从线上告警到主线程卡点定位
android·前端
图扑软件22 分钟前
下篇・换墨|主题/多语言/移动端,一套组件全覆盖
前端·javascript·ui·性能优化·数据可视化
程序员老赵26 分钟前
Docker 部署 TeX Live:轻松搭建 LaTeX 论文排版编译平台
前端·docker·latex
计算机毕设定制辅导-无忧学长27 分钟前
《基于Spring Boot传承之光非遗陶瓷烧造产品交易平台的设计与实现》
java·vue.js·spring boot·后端·毕业设计
Zhu75830 分钟前
部署安全的pg数据库,搭配pgadmin实现web界面管理
前端·数据库·安全
杉氧36 分钟前
打破边界(二):在 React Native 中嵌入原生 UI 组件 (Native UI Components)
android·前端·react native
计算机学姐1 小时前
基于SpringBoot的旅游系统的设计与实现
java·vue.js·spring boot·后端·spring·java-ee·旅游
沧沧凉凉1 小时前
两个小时,AI 把我 2018 年那个 Unity 版保卫萝卜搬进了浏览器
前端·游戏·ai编程