实现渐变背景的气泡框
使用css实现渐变色背景的气泡框
2024-07-19 12:01:00
#实现渐变背景的气泡框
传统的气泡框一般使用一个圆角div+三角形来模拟,在背景色为普通颜色时,分别设置背景色即可达到理想的效果。然而将背景色改为渐变色时,分别给两部分元素设置背景色,无法设置出连续的颜色,所以原先的实现方式不适用。
#初始方案
#实现逻辑
- 画一个渐变色背景的矩形
- 使用和背景色一致的颜色遮罩盖住无需展示的部分,模拟三角形和圆角
- 远离箭头的矩形正常设置border-radius
- 靠近箭头的矩形圆角使用两个遮罩来模拟,使用clip-path的path函数实现
- 使用clip-path的polygon(多边形)函数截取三角形的反转部分,遮住非三角形的部分。
示例代码:
import classNames from "classnames";
import type { PropsWithChildren } from "react";
import styles from "./styles.module.css";
type ArrowDirection = "left" | "right";
interface SimpleLinearPopoverProps {
arrowDirection?: ArrowDirection;
parrentBackground?: string;
}
const SimpleLinearPopover: React.FC<PropsWithChildren<SimpleLinearPopoverProps>> = ({
arrowDirection = "left",
parrentBackground = "#fff",
children,
}) => {
return (
<div
className={classNames(
styles["simple-popover-container"],
styles[`simple-popover-container-direction-${arrowDirection}`]
)}
style={{
"--parent-background-color": parrentBackground,
}}
>
<div className={styles["simple-popover-content"]}>{children}</div>
<div className={styles["simple-popover-arrow"]}></div>
</div>
);
};
export default SimpleLinearPopover;
.simple-popover-container {
position: relative;
width: fit-content;
max-width: 100%;
padding: 12px 24px 12px 16px;
word-break: break-all;
color: #fff;
background: linear-gradient(137deg, #efe2b7 0%, #a4d1b1 90%);
border-radius: 12px;
white-space: pre-wrap;
}
.simple-popover-container.simple-popover-container-direction-left {
transform: rotateY(180deg);
}
.simple-popover-container.simple-popover-container-direction-right {
transform: unset;
}
.simple-popover-content {
overflow: auto;
}
.simple-popover-container.simple-popover-container-direction-left
.simple-popover-content {
transform: rotateY(180deg);
}
/* 上圆角 */
.simple-popover-container:before {
position: absolute;
top: -2px;
right: 6px;
content: "";
width: 14px;
height: 14px;
background: var(--parent-background-color);
clip-path: path("M 0 0 L 0 2 A 12 12 0 0 1 12 14 L 14 14 L 14 0 0 0 Z");
}
/* 下圆角 */
.simple-popover-container:after {
position: absolute;
bottom: -2px;
right: 6px;
content: "";
width: 14px;
height: 14px;
background: var(--parent-background-color);
clip-path: path("M 14 0 L 12 0 A 12 12 0 0 1 0 12 L 0 14 L 14 14 14 0 Z");
}
/* 箭头 */
.simple-popover-arrow:after {
position: absolute;
top: 0;
right: -2px;
content: "";
width: 10px;
height: 100%;
background: var(--parent-background-color);
clip-path: polygon(
100% 0,
0 0,
0 12px,
8px 21px,
0 30px,
0 100%,
100% 100%,
100% 21px
);
}
效果如下:
hello!
hello!
#局限
只能适用于背景色为纯色的场景,对背景为图片或带透明叠加效果的场景不适用
#改良方案
仅用一个div+clip-path,将气泡框描绘出来,由于path函数中只能声明固定坐标点而不能用百分比,而气泡框的宽高需要根据内容自适应,因此需要监听div的大小变化,计算出path的路径。
示例代码:
import classNames from "classnames";
import { PropsWithChildren, useLayoutEffect, useRef, useState } from "react";
import styles from "./styles.module.css";
type ArrowDirection = "left" | "right";
interface LinearPopoverProps {
arrowDirection?: ArrowDirection;
}
const LinearPopover: React.FC<PropsWithChildren<LinearPopoverProps>> = ({
arrowDirection = "left",
children,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
if (containerRef.current) {
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.contentRect) {
const { width, height } = entry.target.getBoundingClientRect();
const path = genPopoverPath(width, height);
containerRef.current!.style["clip-path"] = `path('${path}')`;
}
}
});
resizeObserver.observe(containerRef.current);
return () => {
resizeObserver.disconnect();
};
}
}, []);
return (
<div
ref={containerRef}
className={classNames(
styles["popover-container"],
styles[`popover-container-direction-${arrowDirection}`]
)}
>
<div className="popover-container-content">{children}</div>
</div>
);
};
function genPopoverPath(width: number, height: number) {
const borderRadius = 12;
const arrowWidth = 10;
const path = `M 0 ${borderRadius}
A ${borderRadius} ${borderRadius} 0 0 1 ${borderRadius} 0
L ${width - borderRadius - arrowWidth} 0
A ${borderRadius} ${borderRadius} 0 0 1 ${
width - arrowWidth
} ${borderRadius}
L ${width - arrowWidth} ${borderRadius}
L ${width} 21
L ${width - arrowWidth} 30
L ${width - arrowWidth} ${height - borderRadius}
A ${borderRadius} ${borderRadius} 0 0 1 ${
width - borderRadius - arrowWidth
} ${height}
L ${borderRadius} ${height}
A ${borderRadius} ${borderRadius} 0 0 1 0 ${height - borderRadius}
L 0 ${borderRadius} Z`;
return path.replaceAll("\n", "");
}
export default LinearPopover;
.popover-container {
position: relative;
width: fit-content;
max-width: 100%;
padding: 12px 24px 12px 16px;
word-break: break-all;
color: #fff;
background: linear-gradient(137deg, #efe2b7 0%, #a4d1b1 90%);
white-space: pre-wrap;
}
.popover-container.popover-container-direction-right {
transform: rotateY(180deg);
}
.popover-container.popover-container-direction-right
.popover-container-content {
transform: rotateY(180deg);
}
效果如下:
hello!
hello!