现代 CSS 高阶技巧:实现平滑内凹圆角的工程化实践

通过 数学计算 + CSS mask 复合遮罩 实现的真正几何内凹效果:

背景是一张图片,用来证明中间的凹陷是透明的。


完整代码

app.js

js 复制代码
import FormPage from "./pages/formPage";
import "./App.css";
const App = () => {
  return (
    <div className="box">
      <div className="block"></div>
    </div>
  );
};

export default App;

app.css

css 复制代码
.box {
  background: url(../src/pages/img/10.jpg) 100%;
  height: 500px;
}

.block {
  /* 增大圆角半径 */
  --r: 6px;
  /* 适当增加斜切长度 */
  --s: 69px;
  /* 微调角度使过渡更自然 */
  --a: 44deg;
  
  /* 更柔和的边缘过渡 */
  --_m: 0/calc(2*var(--r)) var(--r) no-repeat radial-gradient(50% 100% at bottom, #000 calc(100% - 0.8px), transparent);

  /* 自动计算的水平偏移 */
  --_d: (var(--s) + var(--r)) * cos(var(--a));


  width: 300px;
  height: 100px;
  background-color: antiquewhite;
  border-radius: 150px;
  mask:
    calc(50% + var(--_d)) var(--_m), calc(50% - var(--_d)) var(--_m),
    radial-gradient(var(--s) at 50% calc(-1*sin(var(--a))*var(--s)),
      transparent 100%, #000 calc(100% + 0.8px)) 0 calc(var(--r)*(1 - sin(var(--a)))),
    linear-gradient(90deg, #000 calc(50% - var(--_d)), transparent 0 calc(50% + var(--_d)), #000 0);
  mask-repeat: no-repeat;
}

整体思路

这段代码的核心思想是:

使用多个 mask 图层组合,通过 radial-gradient 和 linear-gradient 的叠加,形成一个"中间凹进去、边缘过渡柔和"的视觉效果。

它利用了 CSS 中的变量(--r, --s, --a)、三角函数和 mask 属性,实现了动态可配置的内凹圆角效果。


变量解析(CSS Custom Properties)

css 复制代码
--r: 6px;        /* 凹陷区域的半径(控制凹陷大小) */
--s: 69px;       /* 圆弧的半径(控制凹陷的位置) */
--a: 44deg;      /* 角度(用于三角函数计算) */

这些变量可以方便地调整最终的视觉效果。


计算变量(关键逻辑)

--_m:定义一个遮罩图层(更柔和的边缘过渡)

css 复制代码
--_m: 0 / calc(2*var(--r)) var(--r) no-repeat
    radial-gradient(50% 100% at bottom, #000 calc(100% - 0.8px), transparent);
  • 创建一个宽度为 2 * --r,高度为 --r 的径向渐变。
  • 渐变从底部开始,颜色从不透明到透明,形成一个边缘模糊的遮罩条带
  • 这个条带会在最终 mask 中作为"边缘柔化"层使用。

--_d:水平偏移距离(基于三角函数自动计算)

css 复制代码
--_d: (var(--s) + var(--r)) * cos(var(--a));
  • 使用余弦函数计算出一个水平方向上的偏移值。
  • 这个偏移值决定了凹陷区域在水平方向上的位置。
  • 结合下面的 mask 设置,使凹陷区域对称分布在中心两侧。

Mask 图层详解(这是整个效果的关键)

css 复制代码
mask:
  calc(50% + var(--_d)) var(--_m),
  calc(50% - var(--_d)) var(--_m),
  radial-gradient(var(--s) at 50% calc(-1*sin(var(--a))*var(--s)),
    transparent 100%, #000 calc(100% + 0.8px)) 0 calc(var(--r)*(1 - sin(var(--a)))),
  linear-gradient(90deg, #000 calc(50% - var(--_d)), transparent 0 calc(50% + var(--_d)), #000 0);

我们来逐行拆解这四个 mask 图层:


🔹 第一层 & 第二层(边缘柔化层)

css 复制代码
calc(50% + var(--_d)) var(--_m),
calc(50% - var(--_d)) var(--_m)
  • 这两个图层使用的是之前定义好的 --_m 遮罩条带。
  • 分别放置在中心左右各偏移 --_d 的位置。
  • 作用是柔和边缘,避免生硬的裁剪边界。

🔸 第三层(核心凹陷层)

css 复制代码
radial-gradient(var(--s) at 50% calc(-1*sin(var(--a))*var(--s)),
  transparent 100%, #000 calc(100% + 0.8px))
  0 calc(var(--r)*(1 - sin(var(--a))));
  • 创建一个以中心为圆心、向上偏移一定距离的径向渐变。
  • 半径为 --s,位于垂直方向上偏移 sin(a) * s
  • 渐变从透明到黑色,超出部分变为不透明。
  • 最后定位在 0 calc(...),即垂直方向向下偏移一点,让凹陷区域更贴合整体形状。

✅ 这一层是形成"内凹"视觉的核心图层


🔷 第四层(背景遮罩层)

css 复制代码
linear-gradient(90deg, #000 calc(50% - var(--_d)), transparent 0 calc(50% + var(--_d)), #000 0)
  • 水平方向的线性渐变:

    • 左侧和右侧为黑色(显示区域)
    • 中间一段为透明(隐藏区域)
  • 作用是遮住中间的凹陷区域,只保留两边的内容。


最终效果总结

图层 作用
第一、二层 边缘柔化处理(避免锯齿感)
第三层 核心凹陷区域(模拟"内凹"形状)
第四层 背景遮罩(只显示两边,中间隐藏)

结合起来就形成了一个中间凹陷、边缘柔和、对称分布的视觉效果,非常适合用在按钮、卡片等需要轻微凹陷质感的 UI 元素中。


示例效果预览(文字描述)

想象一个椭圆形的盒子(border-radius: 150px),原本是完整的圆形。但在它的正中央,有一个向上弯曲的凹陷区域,就像轻轻按下按钮时那种感觉,边缘还有轻微的阴影过渡。

这种效果常见于 macOS 的菜单栏按钮、iOS 控件等现代 UI 设计中。


如何调整?

你可以通过修改以下变量来实时调整视觉效果:

变量 默认值 调整建议 影响
--r 6px 增大 → 凹陷更大 凹陷区域大小
--s 69px 增大 → 凹陷更深 凹陷位置与深度
--a 44deg 增大 → 凹陷更靠上 凹陷角度与位置
background-color antiquewhite 更浅/深色 整体对比度和质感

在小程序中实现实现平滑内凹圆角


在支付宝小程序中使用 canvas 实现带内凹圆角矩形

小程序的结构:

html 复制代码
<view class="mask-layer">
  <canvas style="width:100%;height:100%" id="canvas" type="2d" onReady="onCanvasReady"></canvas>
</view>

小程序的canvas逻辑:

js 复制代码
Page({
  onCanvasReady() {
    const systemInfo = my.getSystemInfoSync();
    const screenWidth = systemInfo.windowWidth;

    my.createSelectorQuery().select('#canvas').node().exec((res) => {
      const canvas = res[0].node;
      if (!canvas || !canvas.getContext) return;

      const ctx = canvas.getContext('2d');

      // 设置 canvas 像素尺寸(避免模糊)
      canvas.width = screenWidth;
      canvas.height = 200;

      // 动态计算参数
      let width = screenWidth * 0.9;
      let x = (screenWidth - width) / 2; // 居中显示
      let y = 0;
      let height = 100;
      let radius = Math.min(width, height) / 2;
      let indentationWidth = width * 0.35;
      let indentationDepth = 15;

      // 设置颜色
      ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';

      // 调用绘制函数
      this.drawRoundedIndentedRect(ctx, x, y, width, height, radius, indentationWidth, indentationDepth);
    });
  },

  drawRoundedIndentedRect(ctx, x, y, width, height, radius, indentationWidth, indentationDepth) {
    ctx.beginPath();
    ctx.moveTo(x + radius, y);
    ctx.lineTo(x + width / 2 - indentationWidth / 2, y);

    ctx.bezierCurveTo(
      x + width / 2 - indentationWidth / 4, y,
      x + width / 2 - indentationWidth / 4, y + indentationDepth,
      x + width / 2, y + indentationDepth
    );

    ctx.bezierCurveTo(
      x + width / 2 + indentationWidth / 4, y + indentationDepth,
      x + width / 2 + indentationWidth / 4, y,
      x + width / 2 + indentationWidth / 2, y
    );

    ctx.lineTo(x + width - radius, y);
    ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
    ctx.lineTo(x + width, y + height - radius);
    ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
    ctx.lineTo(x + radius, y + height);
    ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
    ctx.lineTo(x, y + radius);
    ctx.quadraticCurveTo(x, y, x + radius, y);

    ctx.closePath();
    ctx.fill();
  }
});

在 css 中使用 canvas 实现平滑内凹圆角

直接展示源码了,和上边小程的差不多

js 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Canvas - 带凹陷的圆角矩形</title>
    <style>
        canvas {
            display: block;
            margin: 40px auto;
            box-shadow: 0px 0px 5px #ccc;
            border-radius: 8px;
            background: url("./截屏2025-05-28 11.15.50.png") no-repeat center center;
            background-size: cover;
        }
    </style>
</head>

<body>
    <canvas id="canvas" width="500" height="200">
        当前浏览器不支持canvas元素,请升级或更换浏览器!
    </canvas>

    <script>
        function draw() {
            const canvas = document.getElementById('canvas');
            if (!canvas || !canvas.getContext) return;

            const ctx = canvas.getContext('2d');

            // 动态设置画布大小
            canvas.width = window.innerWidth * 0.9;
            canvas.height = 150;

            ctx.clearRect(0, 0, canvas.width, canvas.height);

            let width = canvas.width * 0.9;
            let height = 80;
            let x = (canvas.width - width) / 2;
            let y = 40;

            let radius = Math.min(width, height) / 2;
            let indentationWidth = width * 0.7;
            let indentationDepth = 20;

            ctx.fillStyle = '#bfc';
            drawRoundedIndentedRect(ctx, x, y, width, height, radius, indentationWidth, indentationDepth);
        }

        function drawRoundedIndentedRect(ctx, x, y, width, height, radius, indentationWidth, indentationDepth) {
            ctx.beginPath();

            ctx.moveTo(x + radius, y);
            ctx.lineTo(x + width / 2 - indentationWidth / 2, y);

            ctx.bezierCurveTo(
                x + width / 2 - indentationWidth / 4, y,
                x + width / 2 - indentationWidth / 4, y + indentationDepth,
                x + width / 2, y + indentationDepth
            );

            ctx.bezierCurveTo(
                x + width / 2 + indentationWidth / 4, y + indentationDepth,
                x + width / 2 + indentationWidth / 4, y,
                x + width / 2 + indentationWidth / 2, y
            );

            ctx.lineTo(x + width - radius, y);
            ctx.quadraticCurveTo(x + width, y, x + width, y + radius);

            ctx.lineTo(x + width, y + height - radius);
            ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);

            ctx.lineTo(x + radius, y + height);
            ctx.quadraticCurveTo(x, y + height, x, y + height - radius);

            ctx.lineTo(x, y + radius);
            ctx.quadraticCurveTo(x, y, x + radius, y);

            ctx.closePath();
            ctx.fill();
        }

        window.addEventListener('load', draw);
        window.addEventListener('resize', draw);
    </script>
</body>

</html>

嘿嘿!
简单点,直接找UI 要背景图,直接用就好 !!!

相关推荐
加班是不可能的,除非双倍日工资3 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi3 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip3 小时前
vite和webpack打包结构控制
前端·javascript
excel4 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国4 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼4 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy4 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT5 小时前
promise & async await总结
前端
Jerry说前后端5 小时前
RecyclerView 性能优化:从原理到实践的深度优化方案
android·前端·性能优化
画个太阳作晴天5 小时前
A12预装app
linux·服务器·前端