借助CSS实现自适应屏幕边缘的tooltip

欢迎关注我的公众号:前端侦探

tooltip是一个非常常见的交互,一般用于补充文案说明。比如下面这种(蓝色边框表示屏幕边缘)

通常tooltip都会有一个固定的方向,比如top表示垂直居中向上。

但是,如果提示文案比较多,提示区域右比较靠近屏幕边缘,就可能出现这种情况

直接超出屏幕!这很显然是不能接受的。

你可能会想到改变一下对齐方向,比如top-right,但是这里的文案可能是不固定的,也就是会出现这样

嗯...感觉无论怎么对齐都会有局限。那么如何解决呢?一起看看吧

一、理想中的自适应对齐

我们先想想,最完美的对齐是什么样的。

其实没那么复杂,就分两种情况,一个居左,一个居右

1.居左

正常情况下,就是垂直居中朝上

如果提示文本比较多,那就靠左贴近文本容器对齐

如果提示文本继续增加,那就整行换行,并且不超过文本容器

2. 居右

正常情况下,也是垂直居中朝上

如果提示文本比较多,那就靠右贴近文本容器对齐

如果提示文本继续增加,也是整行换行,并且不超过文本容器

那么如何实现这样的对齐方式呢?

二、左自适应对齐的思路

我们先看第一种情况,看似好像有3种对齐方式,而且还要监测是否到了边界,好像挺复杂。其实换个角度,其实是这样一种规则

  1. 当内容较少时,居中对齐
  2. 当内容较多时,居左对齐
  3. 当内容多到换行时,有一个最大宽度

既然涉及到了对齐,那就有对齐的容器和被对齐的对象。

我们可以想象一个虚拟容器,以对齐中心(下图问号图标)向两边扩展,一直到边界处,如下所示(淡蓝色区域)

假设HTML如下

html 复制代码
<span class="tooltip" title="提示"></span>

当气泡文本比较少时,可以通过文本对齐实现居中,气泡可以直接通过伪元素实现

css 复制代码
.tooltip{
  width: 50px; /*虚拟容器宽度,暂时先固定 */
  text-align:center;
}
.tooltip::before{
  content: attr(title);
  display: inline-block;
  color: #fff;
  background-color: #000;
  padding: .5em 1em;
  border-radius: 8px;
  box-sizing: border-box;
}
/*居中箭头*/
.tooltip::after{
  content: '';
  position: absolute;
  width: 1em;
  height: .6em;
  background: #000;
  clip-path: polygon(0 0, 100% 0, 50% 100%);
  top: 0;
  left:0;
  right:0;
  margin: 0 auto;
  transform: translateY(-150%)
}

使用文本居中,也就是text-align: center有个好处,当文本不超过容器时,居中展示,就如同上图展示一样。

当文本比较多时,默认会换行,效果如下

这样应该很好理解吧。

我们需要气泡里的文本在多行时居左,可以直接给气泡设置居左对齐

css 复制代码
.tooltip::before{
  /*...*/
  text-align: left;
}

效果如下

这样就实现了单行居中,多行居左的效果了。

现在还有一个问题,如何在气泡文本较多时,不被对齐容器束缚呢?

首先可以想到的是禁止换行,也就是

css 复制代码
.tooltip::before{
  /*...*/
  white-space: nowrap
}

这样在文本不超过一行时确实可以

看,已经突破了容器束缚。但是文本继续增加时,也会出现无法换行的问题

我们可以想一想,还有什么方式可以控制换行呢?

这里,我们需要设置宽度为最大内容宽度,相当于文本有多少,文本容器就有多宽

css 复制代码
.tooltip::before{
  /*...*/
  width: max-content
}

看似好像和不换行一样

实则不然,我们并没用禁止换行。只要给一个最大宽度,立马就换行了

css 复制代码
.tooltip::before{
  /*...*/
  width: max-content;
  max-width: 300px;
}

效果如下

是不是几乎实现了我们想要的效果了?

不过,这里涉及了两个需要动态计算的宽度,一个是虚拟容器宽度,还有一个是外层最大宽度,

下面看如何实现

三、借助JS计算所需宽度

现如今,外层的最大宽度倒是可以通过容器查询获得,但内部的虚拟容器宽度还无法直接获取,只能借助JS了。

不过我们这里可以先只计算左侧偏移,也就是一半的宽度

具体实现如下

js 复制代码
//问号中心到左侧距离
const x = this.offsetLeft - 8
// 问号的宽度
const w = this.clientWidth
// 外层整行文本容器宽度
const W = this.offsetParent.clientWidth - 32
// 左侧偏移
this.style.setProperty('--x', x + 'px')
// 外层文本容器宽度(气泡最大宽度)
this.style.setProperty('--w', W + 'px')

然后给前面待定的宽度绑定这些变量就行了

css 复制代码
.tooltip{
  /*...*/
  width: calc(var(--x) * 2);
}
.tooltip::before{
  /*...*/
  max-width: var(--w);
}

这样左侧就完全实现自适应了,无需实时计算,仅需初始化一次就好了

四、完全自适应对齐

前面是左侧,那右侧如何判断呢?我们可以比较左侧距离的占比,如果超过一半,就表示现在是居右了

这里用一个属性表示

js 复制代码
this.tooltip.dataset.left = x/W < 0.5 //是否居左

然后就右侧虚拟容器的宽度了,和左侧还有有点不一样

前面我们已经算出了左侧距离,由于超过了一半,所以需要先减然后再乘以二

css 复制代码
.tooltip[data-left="false"]::before{
  /*...*/
  width: calc( (var(--w) - var(--x)) * 2);
  max-width: var(--w);
}

其实这里还是有个小问题的,当气泡文字比较长时,仍然是朝右突破了边界,如下所示

这是因为默认的语言流向造成的(从左往右),解决这个问题也非常简单,仅需改变语言方向就可以了,要用到direction:rtl,如下

css 复制代码
.tooltip[data-left="false"]::before{
  /*...*/
  width: calc( (var(--w) - var(--x)) * 2);
  max-width: var(--w);
  direction: rtl;
}

这样就完美了

现在来看一下所有边界情况的演示

你也可以访问在线demo真实体验:codepen.io/xboxyan/pen...

如果你是 vue3 项目,可以直接用这段封装好的组件(其实没几行代码,大部分自适应都是CSS完成的)

js 复制代码
<!-- 极度自适应的tooltips -->
<script setup lang="ts">
const props = defineProps({
  text: String,
  gap: {
    type: Number,
    default: 12,
  },
})

const show = ref(false)
const pos = reactive({
  x: 0,
  w: 0,
  top: 0,
  gap: 0,
  isLeft: true,
})
const click = (ev: MouseEvent) => {
  // console.log()
  // if (ev.target) {
  //   ev.stopPropagation()
  // }
  const target = ev.target as Element | null
  console.log('xxxxxxxxxxx', target)
  if (target) {
    const { x, y, width } = target.getBoundingClientRect()
    pos.top = y + window.scrollY
    pos.gap = props.gap
    pos.x = x + width / 2 - props.gap
    pos.w = window.innerWidth - props.gap * 2
    show.value = true
  }
}

const wrap = ref<HTMLElement>()

document.body.addEventListener('touchstart', (ev) => {
  // 没有点击当前触发对象就隐藏tooltips
  if (!(wrap.value && ev.target && wrap.value.contains(ev.target as Node))) {
    show.value = false
  }
})
</script>

<template>
  <span class="wrap" ref="wrap" @click="click">
    <slot></slot>
  </span>
  <Teleport to="body">
    <div
      class="tooltip"
      v-show="show"
      :data-title="text"
      :data-left="pos.x / pos.w < 0.5"
      :style="{
        '--x': pos.x + 'px',
        '--top': pos.top + 'px',
        '--gap': pos.gap + 'px',
        '--w': pos.w + 'px',
      }"
    ></div>
  </Teleport>
</template>
<style>
.wrap {
  display: contents;
}
.tooltip {
  position: absolute;
  top: var(--top);
  text-align: center;
  pointer-events: none;
}
.tooltip[data-left='true'] {
  width: calc(var(--x) * 2);
  left: var(--gap);
}
.tooltip[data-left='false'] {
  width: calc((var(--w) - var(--x)) * 2);
  right: var(--gap);
  direction: rtl;
}

.tooltip::before {
  content: attr(data-title);
  display: inline-block;
  color: #fff;
  background-color: #191919;
  padding: 0.5em 0.8em;
  border-radius: 8px;
  transform: translateY(calc(-100% - 0.5em));
  width: max-content;
  max-width: var(--w);
  box-sizing: border-box;
  text-align: left;
}
.tooltip::after {
  content: '';
  position: absolute;
  width: 1.2em;
  height: 0.6em;
  background: #000;
  clip-path: polygon(0 0, 100% 0, 50% 100%);
  top: 0;
  left: 0;
  right: 0;
  margin: 0 auto;
  transform: translateY(calc(-100% - 0.2em));
}
</style>

五、推荐一个开源库

其实市面上有一个库可以完成类似的交互,叫做 float-ui

这个是专门做popover这类交互的,其中有一个shift属性,可以做这种跟随效果

不过对于大部分情况,引入一个单独的库还是成本偏大,建议还是纯原生实现。

这样一个极度自适应的气泡组件,你学会了吗,赶紧在项目中用起来吧~最后,如果觉得还不错,对你有帮助的话,欢迎点赞、收藏、转发 ❤❤❤

相关推荐
一条上岸小咸鱼12 分钟前
Kotlin 基本数据类型(一):Numbers
android·前端·kotlin
前端小巷子36 分钟前
Vue 事件绑定机制
前端·vue.js·面试
uhakadotcom43 分钟前
开源:subdomainpy快速高效的 Python 子域名检测工具
前端·后端·面试
爱加班的猫1 小时前
Node.js 中 require 函数的原理深度解析
前端·node.js
用户8165111263971 小时前
WWDC 2025 Build a SwiftUI app with the new design
前端
伍哥的传说1 小时前
Vue 3.5重磅更新:响应式Props解构,让组件开发更简洁高效
前端·javascript·vue.js·defineprops·vue 3.5·响应式props解构·vue.js新特性
阅文作家助手开发团队_山神1 小时前
第一章: Mac Flutter Engine开发准备工作
前端·flutter
菜牙买菜1 小时前
Hicharts入门
前端·vue.js·数据可视化
摸着石头过河的石头1 小时前
手把手教你入门 MCP:模型上下文协议与 Trae IDE 中的实践
前端·mcp