Skip to main content

封装一个 TriangleButton 组件

· 8 min read

理解 CSS 画三角形的原理,了解 em 单位的最佳实践,并且使用 React 搭配 classnames 封装原子组件

CSS 画三角形

  • 三角形为等腰直角三角形
  • 主要利用的核心属性就是 border

首先,我们先画一个普通的正方形:

div {
width: 100px;
height: 100px;

border: 1px solid skyblue;
}

pic.ssm

下面我们将边框的宽度数值调为调整为 width 的一半:

div {
width: 100px;
height: 100px;

border: solid skyblue;
border-width: 50px;
}

pic.ssm

然后给四个边设置为不同的颜色:

div {
width: 100px;
height: 100px;
border-style: solid;
border-width: 50px;
border-color: orange gold skyblue pink;
}

pic.ssm

接着设置为 border-box

div {
width: 100px;
height: 100px;

border-style: solid;
border-width: 50px;
border-color: orange gold skyblue pink;
box-sizing: border-box;
}

border-box 会让盒子的 width/height 属性控制 border + padding + content 的大小, 由于 margin 为 0,因此目前整个盒子的占位大小为 100px * 100px,全部都由 border 来占位:

pic.ssm

可以看见,整个盒子都是由四个边框构成的,每个边框都是一个 等腰直角三角形, 接下来想办法切割想要的方向的三角形即可。

这里我们来切割蓝色的那个三角形,只需要把其他三个边框设置为透明色:

div {
width: 100px;
height: 100px;

border-style: solid;
border-width: 50px;
border-color: transparent transparent skyblue transparent;

box-sizing: border-box;
}

这样我们就得到了蓝色的三角形:

pic.ssm

但是它的占位有点问题:

pic.ssm

想要把它的高度也调整为 50px,就需要把对边宽度调整为 0,并且把指定的 width/height 去掉:

div {
border-style: solid;
border-width: 0 50px 50px 50px;
border-color: transparent transparent skyblue transparent;

box-sizing: border-box;
}

pic.ssm

这样一来我们就得到了一个完美的 等腰直角三角形。这个三角形的底部等于 border-width * 2,高等于 border-width

下面来总结一下核心要点:

  • 设置怪异盒子模型:box-sizing: border-box
  • 边框设置为实线:border-style: solid
  • 设置边框宽度即三角形的高度,想要的三角形对边宽度设置为 0:border-width: 0 50px 50px 50px
  • 留下想要的三角形的颜色,其余边框设置为透明色:border-color: transparent transparent skyblue transparent

改造三角形

由 font-size 决定大小

在上面我们所画的等腰直角三角形中,它的高度是固定的,现在我想把它改造为一个字体图标,通过 font-size 就能够改变它的大小。

这时需要把 div 替换为一个 span,然后在外层包裹一个 div:

<div class='icon-triangle'>
<span></span>
</div>
.icon-triangle {
display: inline-block;
line-height: 0;

span {
display: inline-block;

border-style: solid;
border-width: 0 1em 1em 1em;
border-color: transparent transparent skyblue transparent;

box-sizing: border-box;
}
}

需要注意的是这里要给外层 div 设置为内联块,并且添加一个 line-height: 0,然后还要给里面的 span 设置为内联块,目的是让外层 div 宽高完全由内部的 span 高度撑开。

最重要的就是把内部 span 的 border-width 的单位设置为 em,作用是让等腰直角三角形的高由 .icon-trianglefont-size 决定。

这样一来,.icon-trianglefont-size 就是等腰三角形的高度了,比如 font-size: 30px,那么这个等腰三角形的高度就是 30px

在直角顶添加一条直线

对于一些视频播放的继续播放按钮,通常是下面这个样子:

pic.ssm

下面我们继续改造我们的三角形,为直角顶添加一条直线:

.icon-triangle {
display: inline-block;
line-height: 0;

span {
position: relative;

display: inline-block;
box-sizing: border-box;
border-style: solid;
border-width: 1em 1em 0em 1em;
border-color: orange transparent transparent transparent;

&::after {
content: "";

position: absolute;
top: 0.2em;
left: -1em;

width: 2em;
border-top: 0.2em solid;
border-top-color: inherit;
}
}
}

在 span 后面加上一个虚元素,然后通过绝对定位,向下移动一段距离即可:

pic.ssm

为了让它由 font-size 决定大小,因此使用的 em 单位,唯一的瑕疵就是这个伪类是脱离了文档流的,整个图标盒子大小没有包含这个伪类进去:

pic.ssm

最后,想要调整这个图标的方向,只需要使用 transform: rotate 来旋转指定的角度即可。

封装 TriangleButton 组件

这里我们使用了 classnames 这个库,它的主要作用就是用来根据传入 React 组件的 props 进行类名拼接从而实现不同的样式。

Props 接口定义

首先,我们给 TriangleButton 定义一个 Props 接口,它基本上抽象出了整个组件的功能:

interface TriangleButtonProps {
// 图标大小
size?: "small" | "normal" | "large" | number;
// 图标颜色
color?: "black" | "white" | "orange" | "skyblue" | "red";
// 是否有垂直线
hasVerticalLine?: boolean;
// 图标方向
direction?: "up" | "right" | "down" | "left";
// 点击事件回调
onClick?: React.MouseEventHandler;
}

组件实现

const TriangleButton: React.FC<TriangleButtonProps> = (props) => {
const { size, color, direction, hasVerticalLine, onClick } = props;

const classes = classnames("triangle-button", {
left: direction === "left",
right: direction === "right",
down: direction === "down",
up: direction === "up",
"small-size": size === "small",
"normal-size": size === "normal",
"large-size": size === "large",
"vertical-line": hasVerticalLine,
});

const sizeStyle = typeof size === "number" ? { fontSize: size + "px" } : {};

const colorStyle = ["black", "white", "orange", "skyblue", "red"].includes(
color
)
? { borderTopColor: color }
: {};

const style = { ...sizeStyle, ...colorStyle };

return (
<div className={classes} onClick={(e) => onClick?.(e)}>
<span style={style}></span>
</div>
);
};

TriangleButton.defaultProps = {
color: "black",
size: "normal",
direction: "left",
hasVerticalLine: false,
};

export default TriangleButton;

样式实现

$direction-map: (
"top": rotate(180deg),
"right": rotate(270deg),
"down": rotate(0deg),
"left": rotate(90deg),
);

$size-map: (
"small-size": 8px,
"normal-size": 16px,
"large-size": 24px,
);

.triangle-button {
display: inline-block;
line-height: 0;
cursor: pointer;

@each $key, $value in $direction-map {
&.#{$key} {
transform: $value;
}
}
@each $key, $value in $size-map {
&.#{$key} {
font-size: $value;
}
}

&.vertical-line {
span::after {
content: "";
position: absolute;

top: 0.2em;
left: -1em;

width: 2em;

border-top: 0.2em solid;
border-top-color: inherit;
}
}

span {
display: inline-block;

border-style: solid;
border-width: 1em 1em 0em 1em;
border-color: black transparent transparent transparent;

box-sizing: border-box;
position: relative;
}
}

Reference