原文:https://www.smashingmagazine.com/2024/03/modern-css-tooltips-speech-bubbles-part1/
简要总结:多年来,Tooltip 是 CSS 中非常常见的一种模式。有许多方法可以处理 CSS 中的 Tooltip ,尽管其中一些需要使用大量神奇数字,让人头痛不已。在本文中,Temani Afif 提出了使用最少的标记和最大的灵活性来创建 Tooltip 的现代技术。
在之前的一篇文章中,我们探讨了带形状的丝带和使用 CSS 渐变和 clip-path()
的不同方法。这一次,我想探索另一种形状,这是您在前端生活中可能至少遇到过一次的:Tooltip。你知道我们在说什么,就是那些看起来像漫画书中的对话框的小东西。它们随处可见,从按钮的悬停效果到你手机上的短信应用。
这些形状乍看起来可能很容易在 CSS 中制作,但最终总是伴随着许多困难。例如,您如何调整尾巴的位置以指示Tooltip 是来自左侧、右侧还是中心位置?在制作 Tooltip 时有许多需要考虑的因素,包括溢出、碰撞检测和语义,但我想要关注的是尾巴的形状和方向,因为我经常看到使用不灵活的固定单位来定位它们。
忘记你已经了解的关于 Tooltip 的知识,因为在本文中,我们将从零开始,您将学习如何使用现代 CSS 构建具有最少标记的 Tooltip,通过调整 CSS 变量来提供组件的灵活性。我们不打算构建一个或两个形状,而是……100 种不同的形状!
这听起来可能像是我们将要进入一个超长的文章,但实际上,我们可以通过调整一些值轻松达到那里。最终,您将拥有一袋满是 CSS 技巧,可以组合在一起创建任何您想要的形状。
猜猜看?我已经创建了一个包含 100 种不同 Tooltip 形状的在线集合,您可以轻松复制并粘贴代码以供您自己使用,但请跟着我。您将想知道如何用最少的代码解锁数百种可能性的秘密。
我们将从形状本身开始讨论,讨论如何通过结合 CSS 渐变和剪裁来去除气泡和尾巴。然后,我们将在第二篇专门致力于改进使用边框和自定义形状的另一种常见 Tooltip 方法。
HTML
我们只使用一个元素:
<div class="tooltip">这里放置您的文本内容</div>
这是挑战:在 HTML 中只使用一个元素,用 CSS 创建数百种 Tooltip 变体。
简单 Tooltip 尾巴
我将直接跳过基本的矩形形状;您知道如何在元素上设置 width
和 height
(或 aspect-ratio
)。让我们从最简单的形状开始,这可以通过仅使用两个 CSS 属性完成:
.tooltip {
/* 尾巴尺寸 */
--b: 2em; /* 底边 */
--h: 1em; /* 高度 */
border-image: fill 0 // var(--h)
conic-gradient(#CC333F 0 0); /* 颜色 */
clip-path:
polygon(0 100%, 0 0, 100% 0, 100% 100%,
calc(50% + var(--b) / 2) 100%,
50% calc(100% + var(--h)),
calc(50% - var(--b) / 2) 100%);
}
border-image
属性创建了一个“溢出的颜色”,而 clip-path
则使用 polygon()
坐标定义了 Tooltip 的形状。(说到 border-image
,我写了一篇深入探讨它,并解释了它可能是唯一支持语法中的双斜杠的 CSS 属性!)
Tooltip 的尾巴位于底部中心,我们有两个变量来控制其尺寸:
我们可以以更直观的方式完成相同的事情,比如定义一个背景然后设置边框(或填
充)以为尾巴创建空间:
background: #CC333F;
border-bottom: var(--h) solid #0000;
…或者使用 box-shadow
(或 outline
)来设置外部颜色:
background: #CC333F;
box-shadow: 0 0 0 var(--h) #CC333F;
虽然这些方法确实更容易,但与我们使用的单一 border-image
声明相比,它们需要额外的声明。而且,我们将在后面看到,border-image
对于完成更复杂的形状非常有用。
这里是一个演示,展示了不同方向的形状,以便您可以看到如何轻松地调整上述代码以更改尾巴的位置。
查看示例 A simple Tooltip using 2 CSS properties by Temani Afif。
接下来,我们将研究包含底部尾巴的形状,但您可以在我的在线集合中轻松找到其他变体。
调整尾巴位置
让我们添加一个第三个变量 --p
,用于控制 Tooltip 的尾巴位置。在上一个示例中,我们在 clip-path
中使用了 50%
,它将尾巴直接定位在 Tooltip 矩形形状底部的水平中心。如果我们为其分配一个变量,我们可以通过将 50%
更新为较小或较大的值来轻松更改 Tooltip 的方向,使其朝向左侧或右侧。
.tooltip {
/* 尾巴尺寸 */
--b: 2em; /* 底边 */
--h: 1em; /* 高度*/
--p: 50%; /* 尾巴位置 */
border-image: fill 0 // var(--h)
conic-gradient(#CC333F 0 0); /* 颜色 */
clip-path:
polygon(0 100%, 0 0, 100% 0, 100% 100%,
calc(var(--p) + var(--b) / 2) 100%,
var(--p) calc(100% + var(--h)),
calc(var(--p) - var(--b) / 2) 100%);
}
--p
变量可以从 0%
到 100%
,其中 0%
与 Tooltip 的左侧对齐,而 100%
与右侧对齐。这里是一个交互式演示,您可以使用范围滑块更新变量:
查看示例 Updating the tail position by Temani Afif。
很棒,对吧?但是,有一个问题。当尾巴的位置设置为极端值时,它似乎会滑出气泡的边缘。请随时在演示中将范围滑块切换为 0%
和 100%
之间,以查看此问题。
Tooltip 的尾巴允许在极端情况下溢出容器。
我们可以通过设置某些值的限制,使尾巴永远不会超出容器。多边形的两个点涉及到修复问题。
这个:
calc(var(--p) + var(--b) / 2) 100%
以及这个:
calc(var(--p) - var(--b) / 2) 100%
第一个 calc()
需要被限制为 100%
,以避免从右侧的溢出,而第二个需要被限制为 0%
,以避免从左侧的溢出。我们可以使用 min()
和 max()
函数来建立范围限制:
`clip-path: polygon(0 100%, 0 0, 100% 0, 100% 100%,`
`min(100%, var(--p) + var(--b) / 2) 100%,`
`var(--p) calc(100% + var(--h)),`
`max(0%, var(--p) - var(--b) / 2) 100%);`
查看示例 Fixing the overflow issue by Temani Afif。
呃!我们已经修复了极端情况,并且现在尾巴不再溢出,而是获得了不同的形状!
Tooltip 的尾巴指向左侧或右侧,这取决于 --x
是正值还是负值。请使用以下演示中的滑块来查看当调整两个变量时, Tooltip 的尾巴是如何重新定位 (--p
) 和重新塑造 (--x
) 的。
查看示例 Updating the tail shape by Temani Afif。
很酷,对吧?如果您曾尝试过自己制作 Tooltip ,我相信您会欣赏这种方法如何消除调整 Tooltip 外观所需的神奇数字。这是一个重要的头痛问题,我们不再需要担心!
您是否注意到,当尾巴被拉伸时,它允许超出容器?那太棒了!使用 min()
和 max()
,我们正确地解决了溢出问题,同时允许尾巴远离容器。
注意,我将 `border
-image的 outset 更新为一个不切实际大的值 (
9999px),而不是使用
--h` 变量。尾巴的形状可以是任何类型的三角形,可以占据更大的区域。由于我们无法知道 outset 的确切值,我们使用这个大值来确保我们有足够的空间填充尾巴的颜色,无论其形状如何。
你觉得 outset 概念看起来奇怪吗?我知道,使用 border-image
并不是我们经常做的事情,所以如果这种方法让你感到困惑,一定要去看看 我的 border-image
文章,详细演示了它的工作原理。
使用渐变处理
大多数麻烦都开始于我们想要用渐变而不是纯色来给 Tooltip 着色。应用单一颜色很简单 —— 即使使用较旧的技术也是如此 —— 但是当涉及到渐变时,使尾巴颜色顺畅地流动到容器的颜色中并不容易。
但猜猜看?这对我们来说不是问题,因为我们已经在 border-image
声明中使用了渐变!
border-image: fill 0 // var(--h)
conic-gradient(#CC333F 0 0);
border-image
只接受渐变或图像,所以为了产生纯色,我不得不使用一个只包含一种颜色的渐变。但是,如果您将其更改为一个“真正”的渐变,使两种或更多颜色之间过渡,那么您就会得到渐变 Tooltip 。就是这样!
查看示例 添加渐变颜色 by Temani Afif。
我们需要注意的唯一一件事是 outset 值。当使用单一颜色时,我们实际上不关心 outset 值是多少;它只需要尽可能大,以覆盖 clip-path
区域,就像我们在设置为 9999px
时所做的那样。但是,当使用多种颜色时,我们不应该使用太大的值,以避免意外剪切渐变。
在上一个演示中,您会注意到我正在使用一个等于 0 0 var(--h) 0
的值,这意味着我们只设置了底部 outset;尾巴在底部,渐变不会在所有方向上扩展,就像其他示例中所做的那样。我不想深入讨论可能出现的所有各种边缘情况,但是如果您在处理渐变颜色时遇到问题,通常是需要检查 border-image
的 outset 值。
使用圆角处理
如果我们尝试在前面的示例中添加 border-radius
,什么都不会发生。那是因为 border-radius
和 border-image
属性并不是非常兼容。我们需要微调 border-image
并将其与 background
结合起来才能使一切正常工作。
我们首先在 .tooltip
上声明 background
和 border-radius
。没有什么花哨的。然后,我们转到 border-image
属性,以便我们可以添加一个略微超出容器底部的条(在最后一个图中用红色突出显示)。这部分有点棘手,我邀请您阅读我之前关于 border-image
的文章,以理解这一段 CSS 魔法。从那里,我们添加 clip-path
并得到最终形状。
.tooltip {
/* 三角形尺寸 */
--b: 2em; /* 底边 */
--h: 1em; /* 高度 */
--p: 50%; /* 位置 */
--r: 1.2em; /* 半径 */
--c: #4ECDC4;
border-radius: var(--r);
clip-path: polygon(0 100%, 0 0, 100% 0, 100% 100%,
min(100%, var(--p) + var(--b) / 2) 100%,
var(--p) calc(100% + var(--h)),
max(0%, var(--p) - var(--b) / 2) 100%);
background: var(--c);
border-image: conic-gradient(var(--c) 0 0) fill 0/
var(--r) calc(100% - var(--p) - var(--b) / 2) 0 calc(var(--p) - var(--b) / 2)/
0 0 var(--h) 0;
}
查看示例 添加圆角 by Temani Afif。
我们现在做得很好,但是当尾巴靠近极端边缘时,仍然存在一个小问题。
这种视觉错误发生在 border-image
与圆角之间重叠时。为了解决这个问题,我们需要根据尾巴的位置 (--p
) 调整 border-radius
值。
我们不会更新所有的半径,只会更新底部的,并且更准确地说是水平值。我想提醒您,border-radius
接受最多八个值 —— 每个角都有两个值,用于设置水平和垂直方向 —— 在我们的案例中,我们将更新左下角和右下角的水平值:
border-radius:
/* 水平值 */
var(--r)
var(--r)
min(var(--r),100% - var(--p) - var(--b)/2) /* 右下角水平 */
min(var(--r),var
(--p) - var(--b)/2) /* 左下角水平 */
/
/* 垂直值 */
var(--r)
var(--r)
var(--r)
var(--r)
所有角的值都等于 --r
,除了左下角和右下角。注意正斜杠 (/
),因为它是分隔水平和垂直半径值的语法的一部分。
现在,让我们深入了解这里发生了什么。对于左下角,当尾巴的位置在右侧时,位置 (--p
) 变量值将很大,以保持半径等于半径 (--r
),这作为最小值。但是当位置接近左侧时,--p
的值会减小,到某个时候,变得小于 --r
的值。结果是半径的值慢慢减小,直到达到 0
。它会随着位置的更新而调整!
我知道这是很多信息,通常需要一个视觉辅助。尝试在下面的演示中慢慢更新尾巴的位置,以更清楚地了解发生了什么。
查看示例 修复边缘情况 by Temani Afif。
对于尾巴为自定义形状的情况呢?我们刚刚使用的技术只适用于尾巴具有两个相等边的情况 —— 也就是等腰三角形。当尾巴具有不同长度的两边时,我们需要调整 border-image
的值,并考虑另一个技巧来使一切再次正确运作。
这一次,边框图像创建了一个水平条,位于元素底部的正下方,并延伸到其边界之外,这样我们就有了足够的颜色来填充尾巴,当尾巴靠近边缘时。
.tooltip {
/* 尾巴尺寸 */
--b: 2em; /* 底边 */
--h: 1.5em; /* 高度 */
--p: 50%; /* 位置 */
--x: 1.8em; /* 尾巴位置 */
--r: 1.2em; /* 半径 */
--c: #4ECDC4;
border-radius: var(--r) var(--r) min(var(--r), 100% - var(--p) - var(--b) / 2) min(var(--r), var(--p) - var(--b) / 2) / var(--r);
clip-path: polygon(0 100%, 0 0, 100% 0, 100% 100%,
min(100%, var(--p) + var(--b) / 2) 100%,
calc(var(--p) + var(--x)) calc(100% + var(--h)),
max(0%, var(--p) - var(--b) / 2) 100%);
background: var(--c);
border-image: conic-gradient(var(--c) 0 0) 0 0 1 0 / 0 0 var(--h) 0 / 0 999px var(--h) 999px;
}
查看示例 具有边框半径的自定义尾巴 by Temani Afif。
再次,border-image
声明看起来很奇怪而且复杂,因为它确实是!如果您想更深入地了解这种方法,请务必参考我的先前文章 —— 您绝对不会后悔的。
“为什么不在我们看过的第一个示例中使用这种方法?” 你可能会问。你是对的,我们可以在第一个示例中使用相同的方法,即使我们没有 --x
变量。尽管如此,我们不选择这个方向的原因是,在某些特定情况下,这种方法存在微小的缺陷,正如您在下图中所看到的。
这就是为什么当我处理简单的等腰三角形时,我不使用这种方法。话虽如此,这种方法完全没问题,大多数情况下,您可能看不到任何视觉故障。
将所有内容组合在一起
我们已经看过了具有相等边的尾巴的 Tooltip,具有尾巴形状更改的尾巴,尾巴位置和方向更改的 Tooltip,具有圆角的 Tooltip 以及填充渐变的 Tooltip。如果我们将所有这些示例结合到一个超级演示中,会是什么样子?
我们可以做到,但不能通过结合我们所涵盖的方法来实现。这次,我们需要另一种方法,这次使用伪元素。对于这个,我保证不使用 border-image
!
.tooltip {
/* 三角形尺寸 */
--b: 2em; /* 底边 */
--h: 1em; /* 高度 */
--p: 50%; /* 位置 */
--r: 1.2em; /* 半径 */
border-radius: var(--r) var(--r) min(var(--r), 100% - var(--p) - var(--b) / 2) min(var(--r), var(--p) - var(--b) / 2) / var(--r);
background: 0 0 / 100% calc(100% + var(--h))
linear-gradient(60deg, #CC333F, #4ECDC4); /* 渐变 */
position: relative;
z-index: 0;
}
.tooltip:before {
content: "";
position: absolute;
z-index: -1;
inset: 0 0 calc(-1*var(--h));
background-image: inherit;
clip-path:
polygon(50% 50%,
min(100%, var(--p) + var(--b) / 2) calc(100% - var(--h)),
var(--p) 100%,
max(0%, var(--p) - var(--b) / 2) calc(100% - var(--h)));
}
伪元素用于在底部创建尾巴,注意它是如何继承主元素的渐变来模拟一个覆盖整个形状的连续渐变的。
另一个要注意的重要事项是在 .tooltip
中声明的 background-size
。由于伪元素的负底部值覆盖了更大的区域,因此我们必须增加渐变的高度,以便它覆盖相同的区域。
查看示例 渐变和边框半径 by Temani Afif。
对于自定义尾巴形状,我们需要稍微调整代码,以考虑 Tooltip 左右两侧的溢出。想法是当尾巴即将离开容器时,增加
渐变的区域。
.tooltip {
--p: 50%; /* 位置 */
--x: -2em; /* 尾巴形状和方向 */
--_e: max(0%, -1 * var(--x) - var(--p), var(--x) + var(--p) - 100%);
background:
50% 0 / calc(100% + 2*var(--_e)) calc(100% + var(--h))
linear-gradient(60deg, #CC333F, #4ECDC4); /* 渐变 */
}
.tooltip:before {
inset: 0 calc(-1 * var(--_e)) calc(-1 * var(--h));
padding-inline: var(--_e);
}
除了控制尾巴形状和方向的 --x
变量外,我引入了一个新变量 --_e
,用于定义渐变的宽度以覆盖 .tooltip
,以及伪元素的内联填充和其左右值。这可能看起来是一个复杂的配置,但是 --_e
的想法是,在大多数情况下,它将等于 0
,这样我们就可以获得与我们上一个示例相同的代码。但是当尾巴溢出 .tooltip
容器时,--_e
的值会增加,这样就增加了渐变的区域,以覆盖溢出部分。
在下面的演示中尝试改变尾巴的位置和形状,注意当尾巴溢出侧边时渐变的变化。
查看示例 具有边框半径和渐变的自定义尾巴 by Temani Afif。
我知道最后的代码可能看起来复杂(前面的一些也是如此),因此我创建了一个 Tooltip 的在线收集,您可以从中轻松获取代码。我尽量涵盖尽可能多的情况,甚至是您可能永远不需要的情况。话虽如此,了解如何构建各种 Tooltip 形状是很有好处的。
最后一点思考
如果我们统计一下,我们已经制作了 32 种不同的 Tooltip 形状。这是两种颜色(实色或渐变)、两种角(尖角或圆角),分别产生了四种变化,两种尾巴形状(等腰三角形和自定义)产生了另外两种变化,以及四种不同的尾巴位置(顶部、底部、左侧和右侧),总共是 32 种 Tooltip 变体。
我们研究的最后一个示例可以通过调整不同的变量来生成所有形状。
我知道你在想什么:为什么我不简单地分享最后的代码片段并结束呢?这篇文章真的需要这么长吗,当我们本可以直接进入解决方案吗?
当然,我们可以这样做,但是如果您比较一下仅使用两个 CSS 属性的第一个示例和最后一个示例的代码,您会发现,用更少的行代码就能实现更复杂的效果。我们从一个基本的 Tooltip 形状开始,然后踏上了一段旅程,使其考虑到更复杂的 Tooltip 类型。此外,我们还学会了许多在其他情况下可能有用的技巧,不一定只用于创建 Tooltip。
结论
这是本系列的第一部分的全部内容。在继续阅读第二部分之前,请花点时间消化我们在第一部分中涵盖的内容。事实上,为了帮助您准备第二部分,这里有一个小作业:尝试使用本文学到的 CSS 技巧创建以下 Tooltip。
你能搞定吗?如果需要参考,其中所有 Tooltip 的代码都包含在我的 Tooltip 收集中,但是请尝试自己做 —— 这是很好的练习!也许你会找到一个与我的不同(或者可能更好)的方法。