CSS & JS Effect – 画箭头

前言

好一阵子没有写 HTML、CSS 了,都生疏了。

今天要分享的是用 HTML + CSS 画箭头,最终效果长这样:

image

为什么要自己画箭头呢?

干嘛不使用 icon?

因为...我误以为箭头很容易画🙄

 

Step by Step 画箭头

我的画法很直接,但未必是聪明的画法哦。

第一步:elements

image

首先要 3 个 elements。

那就一个 div 加上 ::before ::after 吧。

HTML

<div class="arrow"></div>

Styles

:root {
  --arrow-body-length: 375px;
  --arrow-body-thickness: 60px;
  --arrow-head-length: 250px;
  --arrow-head-thickness: 80px;
}

.arrow {
  width: var(--arrow-body-length);
  height: var(--arrow-body-thickness);

  opacity: 0.5;
  background-color: red;

  &::before,
  &::after {
    content: '';
    display: block;

    width: var(--arrow-head-length);
    height: var(--arrow-head-thickness);

    opacity: 0.5;
  }

  &::before {
    background-color: green;
  }

  &::after {
    background-color: blue;
  }
}

只是给了一些颜色和尺寸,看得见 elements 先,目前长这样

image

第二步:基本布局

.arrow {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-end;

  &::before,
  &::after {
    flex-shrink: 0;
  }
}

效果

image

第三步:rotate

接着把 ::before 和 ::after 做一个 rotate 35° 整体形状就出来了。

:root {
  --arrow-head-rotate: 35deg;
}

.arrow {
  &::before {
    transform: rotate(var(--arrow-head-rotate));
  }

  &::after {
    transform: rotate(calc(-1 * var(--arrow-head-rotate)));
  }
}

注:我刻意用了 35° 是因为 45° 太容易算,而真实场景不一定那么刚好会是 45°。

效果

image

哎哟,这里细节就来了哦!

默认的 transform-origin 是 center,因此 rotate 35° 是这样发生的

imageimage

显然这不是我们期望的,我们调整 transform-origin 看看

.arrow {
  &::before {
    transform-origin: right bottom;
  }

  &::after {
    transform-origin: right top;
  }
}

注意看它的 rotate 的方式

image

再来试试这样

.arrow {
  &::before {
    transform-origin: right top;
  }

  &::after {
    transform-origin: right bottom;
  }
}

效果

image

可以看到,不同的 transform-origin 效果是不同的。

那要用哪一个呢?

不急,我们先往下一步走走看

第四步:translateY

不管 transform-origin 是 top bottom 还是 bottom top

imageimage

中间都有一段间隔。

我们需要把间隔合起来,这样才能形成箭头的尖角。

用 translateY 将它们重合起来(先看 origin top bottom):

image

上面的往下移,下面的往上移。

为什么是 80px 呢?

80px 指的是箭头的厚度(--arrow-head-thickness)

因为它是一个圈

image

箭头的厚度就是圈的半径(radius)80px

.arrow {
  --translate-y: var(--arrow-head-thickness);

  &::before {
    transform-origin: right top;
    transform: translateY(var(--translate-y)) rotate(var(--arrow-head-rotate));
  }

  &::after {
    transform-origin: right bottom;
    transform: translateY(calc(-1 * var(--translate-y))) rotate(calc(-1 * var(--arrow-head-rotate)));
  }
}

效果

image

整体箭头形状出来了,但有好几处明显是过头了,需要一些裁剪 / 移位。

我们换另一个 origin bottom top 看看是否有相同问题,或是其它问题。

image

不同 origin 出来的位置不同,因此 translateY 的移动也需要不同计算方式。

image

把它看作一个 right triangle

hypot 长度是 80px(--arrow-head-thickness)

角度是 55°(因为箭头 rotate 是 35°)

求,opposite 的长度。

算式 opposite = sin(θ) * hypot → sin(55°) * 80 = 65.532...

注:对 right triangle 几何公式不熟悉的读者,可以看这篇

.arrow {
  --translate-y: calc(sin(90deg - var(--arrow-head-rotate)) * var(--arrow-head-thickness));

  &::before {
    transform-origin: right bottom;
    transform: translateY(var(--translate-y)) rotate(var(--arrow-head-rotate));
  }

  &::after {
    transform-origin: right top;
    transform: translateY(calc(-1 * var(--translate-y))) rotate(calc(-1 * var(--arrow-head-rotate)));
  }
}

效果

image

相同的问题 -- 好几处明显是过头了,需要一些裁剪 / 移动。

好,我们挑一个来处理就好。

image

如果箭头 rotate 是 >= 45° 就不会出现过头的问题,由于我们是 35° 所以过头了。

箭头蓝绿的部分需要 clip 掉。

image

箭头 rotate 35°,厚度 80px,求 opposite 长度。

算式

hypot = 80 / cos(20°) = 85.134... 

opposite = sin(20°) * 85.134 = 29.117...

Styles

.arrow {
  --clip-degree: calc(90deg - calc(2 * min(var(--arrow-head-rotate), 45deg)));
  --clip-hypot: calc(var(--arrow-head-thickness) / cos(var(--clip-degree)));
  --clip-opposite: calc(sin(var(--clip-degree)) * var(--clip-hypot));

  &::before {
    clip-path: polygon(0 0, 100% 0, calc(100% - var(--clip-opposite)) 100%, 0 100%);
  }

  &::after {
    clip-path: polygon(0 0, calc(100% - var(--clip-opposite)) 0, 100% 100%, 0 100%);
  }
}

效果

image

箭头 clip 正确了,但箭身任然过头。

这我们不能用 clip,因为箭头伪元素在箭身里,clip 箭身会导致上下箭头超出箭身的部分也被 clip 掉。

正确的做法是减少箭身的 width 长度。

image

角度是 20° + 35°

adjacent 是箭身一半厚度 30px

求 opposite

.arrow {
  --move-degree: calc(var(--arrow-head-rotate) + (90deg - (2 * var(--arrow-head-rotate))));
  --move-adjacent: calc(var(--arrow-body-thickness) / 2);
  --move-hypot: calc(var(--move-adjacent) / cos(var(--move-degree)));
  --move-opposite: calc(sin(var(--move-degree)) * var(--move-hypot));

  width: calc(var(--arrow-body-length) - var(--move-opposite));
}

效果

image

箭身短的同时,还得用 translateX 把箭头往前(右)推。

.arrow {
  &::before {
    transform: translateX(var(--move-opposite)) translateY(var(--translate-y)) rotate(var(--arrow-head-rotate));
  }

  &::after {
    transform: translateX(var(--move-opposite)) translateY(calc(-1 * var(--translate-y)))
      rotate(calc(-1 * var(--arrow-head-rotate)));
  }
}

效果

gif

注:箭头 rotate 太小会坏掉,但无所谓,毕竟太小太大在真实项目不太可能出现。

完整代码

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;

  *:focus {
    outline: none;
  }
}

:root {
  --arrow-body-length: 375px;
  --arrow-body-thickness: 60px;
  --arrow-head-length: 250px;
  --arrow-head-thickness: 80px;
  --arrow-head-rotate: 35deg;
}

.arrow {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-end;

  --move-degree: calc(var(--arrow-head-rotate) + (90deg - (2 * var(--arrow-head-rotate))));
  --move-adjacent: calc(var(--arrow-body-thickness) / 2);
  --move-hypot: calc(var(--move-adjacent) / cos(var(--move-degree)));
  --move-opposite: calc(sin(var(--move-degree)) * var(--move-hypot));

  width: calc(var(--arrow-body-length) - var(--move-opposite));
  height: var(--arrow-body-thickness);

  opacity: 0.5;
  background-color: red;

  &::before,
  &::after {
    flex-shrink: 0;
    content: '';
    display: block;

    width: var(--arrow-head-length);
    height: var(--arrow-head-thickness);

    opacity: 0.5;
  }

  --clip-degree: calc(90deg - calc(2 * min(var(--arrow-head-rotate), 45deg)));
  --clip-hypot: calc(var(--arrow-head-thickness) / cos(var(--clip-degree)));
  --clip-opposite: calc(sin(var(--clip-degree)) * var(--clip-hypot));

  &::before {
    transform-origin: right top;
    background-color: green;
    clip-path: polygon(0 0, 100% 0, calc(100% - var(--clip-opposite)) 100%, 0 100%);
    transform: translateX(var(--move-opposite)) translateY(var(--arrow-head-thickness)) rotate(var(--arrow-head-rotate));
  }

  &::after {
    transform-origin: right bottom;
    background-color: blue;
    clip-path: polygon(0 0, calc(100% - var(--clip-opposite)) 0, 100% 100%, 0 100%);
    transform: translateX(var(--move-opposite)) translateY(calc(-1 * var(--arrow-head-thickness)))
      rotate(calc(-1 * var(--arrow-head-rotate)));
  }
}
View Code

第五步:border-radius

箭头尖尖看起来很可怕,我们让它圆润起来。

.arrow {
  border-radius: 999px;

  &::before,
  &::after {
    border-radius: 999px;
  }
}

效果

image

完全是错的,因为尖角和圆角的做法完全不一样。

我们回到最初的起点,重新看圆角如何处理。

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;

  *:focus {
    outline: none;
  }
}

body {
  --body-padding-top: 200px;
  --body-padding-left: 240px;
  padding-left: var(--body-padding-left);
  padding-top: var(--body-padding-top);
}

.wall {
  position: fixed;
  top: var(--body-padding-top);
  left: calc(var(--body-padding-left) + var(--arrow-body-length));
  transform: translateY(-50%);
  width: 100px;
  height: 300px;
  background-color: black;
  opacity: 0.5;
}

:root {
  --arrow-body-length: 375px;
  --arrow-body-thickness: 60px;
  --arrow-head-length: 250px;
  --arrow-head-thickness: 80px;
  --arrow-head-rotate: 28deg;
}

.arrow {
  border-radius: 999px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-end;

  width: var(--arrow-body-length);
  height: var(--arrow-body-thickness);

  opacity: 0.5;
  background-color: red;

  &::before,
  &::after {
    flex-shrink: 0;
    border-radius: 999px;
    content: '';
    display: block;

    width: var(--arrow-head-length);
    height: var(--arrow-head-thickness);

    opacity: 0.5;
  }

  &::before {
    transform-origin: right top;
    background-color: green;
    transform: rotate(var(--arrow-head-rotate));
  }

  &::after {
    transform-origin: right bottom;
    background-color: blue;
    transform: rotate(calc(-1 * var(--arrow-head-rotate)));
  }
}
View Code

效果

image

先用 translateY 把中间的间隔合起来。

imageimage

那 translate y 具体是多少呢?如何计算?

我们简化它来看

image

在 rotate 之前,箭头中心和箭身中心的间隔是半个箭头的厚度,这就是 translate y 的距离。

rotate 28° 之后

image

中心到中心的距离变长了,因此我们需要计算出它变长了多少。

image

中心到中心的距离 = origin to center 80px - right triangle 的 adjacent

right triangle 的角度是 28°(箭头 rotate) + 45°(hypot 是从左上角到中心,刚好是 90° 的一半 45°)= 73°

hypot 的长度是 √(40² + 40²) = 56.568...,40 来自箭头厚度 80 的一半。

最后算 adjacent = cos(73°) * 56.568 = 16.539...

translate y = 80 - 16.539 = 63.460...

.arrow {
  --translate-y: calc(
    var(--arrow-head-thickness) -
      (cos(var(--arrow-head-rotate) + 45deg) * hypot(var(--arrow-head-thickness) / 2, var(--arrow-head-thickness) / 2))
  );

  &::before {
    transform: translateY(var(--translate-y)) rotate(var(--arrow-head-rotate));
  }

  &::after {
    transform: translateY(calc(-1 * var(--translate-y))) rotate(calc(-1 * var(--arrow-head-rotate)));
  }
}

效果

gif

下一个要解决的问题是

image

过头的箭身。

解决方式和处理尖角一样,让箭头往前(右)移。

如何计算呢?

我们简化看看

imageimage

我们的任务是计算出那一段间距。

image

关键在 right triangle(我们要计算出的距离是那一小段蓝线)

角度是 28°(箭头 rotate) + 45°(hypot 是从左上角到中心,刚好是 90° 的一半 45°)= 73°

hypot 的长度是 √(40² + 40²) = 56.568...,40 来自箭头厚度 80 的一半。

先算出 opposite,再减去圈的 radius(箭头厚度的一半 40px)就是我们要的间距长度了。

opposite = sin(73°) * 56.568 = 54.096...

gap = opposite - radius → 54.096 - 40 = 14.096

.arrow {
  --translate-y: calc(
    var(--arrow-head-thickness) -
      (cos(var(--arrow-head-rotate) + 45deg) * hypot(var(--arrow-head-thickness) / 2, var(--arrow-head-thickness) / 2))
  );
  --translate-x: calc(
    (sin(var(--arrow-head-rotate) + 45deg) * hypot(var(--arrow-head-thickness) / 2, var(--arrow-head-thickness) / 2)) -
      (var(--arrow-head-thickness) / 2)
  );

  &::before {
    transform: translateX(var(--translate-x)) translateY(var(--translate-y)) rotate(var(--arrow-head-rotate));
  }

  &::after {
    transform: translateX(var(--translate-x)) translateY(calc(-1 * var(--translate-y)))
      rotate(calc(-1 * var(--arrow-head-rotate)));
  }
}

效果

gif

由于箭身也是圆角,因此不需要像处理尖角那样减少箭身长度。

但!如果箭身是尖角呢?

.arrow {
  border-radius: unset;
}

效果

image

计算出红线的长度,缩短箭身,把箭头再往右移。

image

这题视乎比较难算,无法用简单的 right triangle 规则算出来,必须使用圆形方程。

圆圈的 radius 是箭头厚度的一半,80 / 2 = 40px

以圆圈的中心作为几何的中心点,坐标 0, 0。

蓝线是一条横线,它的 y 坐标是箭身厚度的一半 60 / 2 = 30

求,y 坐标 30 与圆圈的交点(会有两个,我们只需要 positive 的那一个)。

最后拿 radius 减掉交点 x 就是蓝线的长度了。

算式

(x - h)² + (y - k)² = r²

→ (x - h)² = r² - (y - k)²

→ x - h = √(r² - (y - k)²)

→ x = √(r² - (y - k)²) + h

→ √(40² - (30 - 0)²) + 0

→ √(1600 - 900) + 0 = 26.457...

length = 40 - 26.457 = 13.542...

Styles

.arrow {
  --extra-translate-x-due-to-arrow-body-non-radius: calc(
    (var(--arrow-head-thickness) / 2) - sqrt(
        (pow(var(--arrow-head-thickness) / 1px / 2, 2) - pow(var(--arrow-body-thickness) / 1px / 2, 2))
      ) *
      1px
  );
 
  width: calc(var(--arrow-body-length) - var(--extra-translate-x-due-to-arrow-body-non-radius));

  &::before {
    transform: translateX(calc(var(--translate-x) + var(--extra-translate-x-due-to-arrow-body-non-radius)))
      translateY(var(--translate-y)) rotate(var(--arrow-head-rotate));
  }

  &::after {
    transform: translateX(calc(var(--translate-x) + var(--extra-translate-x-due-to-arrow-body-non-radius)))
      translateY(calc(-1 * var(--translate-y))) rotate(calc(-1 * var(--arrow-head-rotate)));
  }
}

提醒:sqrt 参数必须是 number,不可以带 unit,比如 10px 不行,要 10 才可以。

效果

gif

完整代码

:root {
  --arrow-body-length: 375px;
  --arrow-body-thickness: 60px;
  --arrow-head-length: 250px;
  --arrow-head-thickness: 80px;
  --arrow-head-rotate: 28deg;
}

.arrow {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-end;

  height: var(--arrow-body-thickness);

  opacity: 0.5;
  background-color: red;

  &::before,
  &::after {
    flex-shrink: 0;

    border-radius: 999px;
    content: '';
    display: block;

    width: var(--arrow-head-length);
    height: var(--arrow-head-thickness);

    opacity: 0.5;
  }

  --translate-y: calc(
    var(--arrow-head-thickness) -
      (cos(var(--arrow-head-rotate) + 45deg) * hypot(var(--arrow-head-thickness) / 2, var(--arrow-head-thickness) / 2))
  );
  --extra-translate-x-due-to-arrow-body-non-radius: calc(
    (var(--arrow-head-thickness) / 2) - sqrt(
        (pow(var(--arrow-head-thickness) / 1px / 2, 2) - pow(var(--arrow-body-thickness) / 1px / 2, 2))
      ) *
      1px
  );
  --translate-x: calc(
    (sin(var(--arrow-head-rotate) + 45deg) * hypot(var(--arrow-head-thickness) / 2, var(--arrow-head-thickness) / 2)) -
      (var(--arrow-head-thickness) / 2)
  );

  width: calc(var(--arrow-body-length) - var(--extra-translate-x-due-to-arrow-body-non-radius));

  &::before {
    transform-origin: right top;
    background-color: green;
    transform: translateX(calc(var(--translate-x) + var(--extra-translate-x-due-to-arrow-body-non-radius)))
      translateY(var(--translate-y)) rotate(var(--arrow-head-rotate));
  }

  &::after {
    transform-origin: right bottom;
    background-color: blue;
    transform: translateX(calc(var(--translate-x) + var(--extra-translate-x-due-to-arrow-body-non-radius)))
      translateY(calc(-1 * var(--translate-y))) rotate(calc(-1 * var(--arrow-head-rotate)));
  }
}
View Code

 

总结

本篇讲解如何用纯 HTML + CSS 实现箭头。

手法虽然有点粗糙,但勉强还是能用的。

 

posted @ 2025-12-02 08:10  兴杰  阅读(1)  评论(0)    收藏  举报