Skip to content

教程

¥Tutorial

从头开始构建工具提示并学习如何使用 Floating UI 的定位工具包。

¥Build a tooltip from scratch and learn how to use Floating UI’s positioning toolkit.

本页面通过从头开始构建工具提示来教你 Floating UI 定位的基础知识。如果你只是想开始学习 API,请跳至 下一节

¥This page teaches you the fundamentals of Floating UI’s positioning by building a tooltip from scratch. If you just want to start learning about the API, skip to the next section.

继续之前

¥Before proceeding

该库的 vanilla 包是 “锚定位” 的基于 JavaScript 的布局引擎。它充当 CSS 的填充,以在文档上正确定位绝对定位的锚定元素。

¥The vanilla package of this library is a JavaScript-based layout engine for “anchor positioning”. It acts as a polyfill for CSS to correctly position absolutely-positioned anchored elements on the document.

如果你正在寻找预构建的组件或开箱即用的简单组件,你可能会发现其他库更适合你的用例。

¥If you’re looking for pre-built components or something simple out of the box, you may find other libraries are better suited for your use case.

配置

¥Setting up

创建一个包含两个元素的新 HTML 文档:<button> 和工具提示 <div>

¥Create a new HTML document with two elements, a <button> and a tooltip <div>:

<!doctype html>
<html>
  <head>
    <title>Floating UI Tutorial</title>
  </head>
  <body>
    <button id="button" aria-describedby="tooltip">
      My button
    </button>
    <div id="tooltip" role="tooltip">My tooltip</div>
 
    <script type="module">
      // Your code will go here.
    </script>
  </body>
</html>

现在你应该看到以下内容(浏览器的默认样式除外):

¥Right now you should see the following (except with the browser’s default styling):

My tooltip

样式

¥Styling

让我们给我们的工具提示一些样式:

¥Let’s give our tooltip some styling:

<!doctype html>
<html>
  <head>
    <title>Floating UI Tutorial</title>
    <style>
      #tooltip {
        background: #222;
        color: white;
        font-weight: bold;
        padding: 5px;
        border-radius: 4px;
        font-size: 90%;
      }
    </style>
  </head>
  <body>
    <!-- ... -->
  </body>
</html>

这是到目前为止的结果:

¥Here’s the result so far:

My tooltip

使工具提示 “浮动”

¥Making the tooltip “float”

与任何其他元素一样,工具提示 <div> 是文档上的常规块,这就是它跨越页面整个宽度的原因。

¥Your tooltip <div> is a regular block on the document, like any other element, which is why it spans the whole width of the page.

不过,我们希望它漂浮在 UI 之上,这样它就不会扰乱文档的流程,并且只占用与其内容一样多的大小。

¥We want it to float on top of the UI though, so it doesn’t disrupt the flow of the document, and should only take up as much size as its contents.

#tooltip {
  width: max-content;
  position: absolute;
  top: 0;
  left: 0;
  background: #222;
  color: white;
  font-weight: bold;
  padding: 5px;
  border-radius: 4px;
  font-size: 90%;
}

访问 初始布局 了解更多信息。

¥Visit Initial layout to learn more.

My tooltip

你的工具提示现在是一个 “浮动” 元素 - 它只占用所需的大小,并覆盖在 UI 顶部。

¥Your tooltip is now a “floating” element — it only takes up as much size as it needs to and is overlaid on top of the UI.

定位

¥Positioning

在模块 <script> 标签内,添加以下代码:

¥Inside your module <script> tag, add the following code:

在 CodeSandbox 上玩

¥Play on CodeSandbox

import {computePosition} from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.6.3/+esm';
 
const button = document.querySelector('#button');
const tooltip = document.querySelector('#tooltip');
 
computePosition(button, tooltip).then(({x, y}) => {
  Object.assign(tooltip.style, {
    left: `${x}px`,
    top: `${y}px`,
  });
});

上面,我们使用按钮和工具提示元素作为参数调用 computePosition()

¥Above, we call computePosition() with the button and tooltip elements as arguments.

它返回 Promise,因此我们使用 .then() 方法,该方法为我们传递计算出的 xy 坐标,我们用它们为工具提示分配 lefttop 样式。

¥It returns a Promise, so we use the .then() method which passes in the calculated x and y coordinates for us, which we use to assign left and top styles to the tooltip.

我们的工具提示现在位于按钮下方的中心:

¥Our tooltip is now centered underneath the button:

My tooltip

位置

¥Placements

默认位置是 'bottom',但你可能希望将工具提示放置在与按钮相关的任何位置。为此,Floating UI 有 placement 选项,作为第三个参数传递到选项对象中:

¥The default placement is 'bottom', but you probably want to place the tooltip anywhere relative to the button. For this, Floating UI has the placement option, passed into the options object as a third argument:

import {computePosition} from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.6.3/+esm';
 
const button = document.querySelector('#button');
const tooltip = document.querySelector('#tooltip');
 
computePosition(button, tooltip, {
  placement: 'right',
}).then(({x, y}) => {
  Object.assign(tooltip.style, {
    left: `${x}px`,
    top: `${y}px`,
  });
});
My tooltip

可用的基本位置为 'top''right''bottom''left'

¥The available base placements are 'top', 'right', 'bottom', 'left'.

每个碱基位置都有 -start-end 形式的对齐方式。例如,'right-start''bottom-end'。这些允许你将工具提示与按钮的边缘对齐,而不是使其居中。

¥Each of these base placements has an alignment in the form -start and -end. For example, 'right-start', or 'bottom-end'. These allow you to align the tooltip to the edges of the button, rather than centering it.

我们的第一个问题

¥Our first problem

这些位置本身就是一个有用的功能,但它们并没有提供太多使用原始 CSS 技巧无法实现的功能。这给我们带来了第一个问题:如果我们使用 'top' 展示位置会发生什么?

¥These placements are a useful feature themselves, but they don’t offer much that couldn’t be achieved with raw CSS tricks. Which brings us to our first problem: what happens if we use a 'top' placement?

My tooltip

我们无法读取工具提示文本,因为该按钮恰好靠近文档边界。在这种情况下,你可以使用 'bottom' 放置,而不是 'top'。同样,你可以仅使用 CSS 来实现此目的,尽管它可能会变得笨拙并且需要额外的父封装标签。

¥We can’t read the tooltip text because the button happens to be close to the document boundary. In this case, you could use the 'bottom' placement, instead of 'top'. Again, you could just use CSS for this, although it may become unwieldy and require extra parent wrapper tags.

需要为应用中添加的每个工具提示手动处理此问题可能很麻烦。当你想随心所欲地将工具提示或弹出框应用到页面上任何位置的元素,并且将其位置设置为 “只是工作” 以适应任何屏幕尺寸或位置,而无需进行任何调整时,尤其如此。

¥Needing to manually handle this for every single tooltip you add in an application can be cumbersome. This is especially true when you want to apply a tooltip or popover to an element anywhere on the page at a whim, and have its position “just work” for any screen size or location, without needing to adjust anything.

这就是为什么你可以通过采用 “中间件” 让 Floating UI 自动为你处理它。

¥This is why you can let Floating UI handle it for you automatically with the adoption of “middleware”.

中间件

¥Middleware

中间件是实现基本放置定位之外的每个功能的方式。

¥Middleware are how every single feature beyond the basic placement positioning is implemented.

中间件是在 computePosition() 的调用和最终返回之间运行的一段代码,用于修改数据或向消费者提供数据。

¥A middleware is a piece of code which runs between the call of computePosition() and its eventual return, to modify or provide data to the consumer.

flip() 为我们修改坐标,以便它自动使用 'bottom' 放置,而无需我们显式指定它。

¥flip() modifies the coordinates for us such that it uses the 'bottom' placement automatically without us needing to explicitly specify it.

import {
  computePosition,
  flip,
} from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.6.3/+esm';
 
const button = document.querySelector('#button');
const tooltip = document.querySelector('#tooltip');
 
computePosition(button, tooltip, {
  placement: 'top',
  middleware: [flip()],
}).then(({x, y}) => {
  Object.assign(tooltip.style, {
    left: `${x}px`,
    top: `${y}px`,
  });
});

现在,即使我们将 placement 设置为 'top',如果工具提示无法放在顶部,它也会自动翻转到底部。无需手动调整任何内容。

¥Now, even though we’ve set placement to 'top', the tooltip flips to the bottom automatically for us if it can’t fit on the top. No need to adjust anything manually.

My tooltip

重要的是,如果可以的话,它将始终继续使用 'top' 布局,只有在必要时才回退到 'bottom'

¥Importantly, it will continue to use the 'top' placement at all times if it can, and only fallback to 'bottom' if it has to.

转移中间件

¥Shift middleware

现在,如果我们想在工具提示中包含更多内容该怎么办?

¥Now, what if we wanted to have more content inside the tooltip?

<div id="tooltip" role="tooltip">
  My tooltip with more content
</div>
My tooltip with more content

哦不,我们现在遇到了和以前一样的问题,但是在相反的轴上(x 而不是 y)。因为它位于按钮下方的中心,并且工具提示恰好比按钮宽,所以它最终会溢出视口边缘。

¥Oh no, we now have the same problem as before, but on the opposite axis (x instead of y). Because it’s centered beneath the button, and the tooltip happens to be wider than the button, it ends up overflowing the viewport edge.

为了解决这个问题,我们有了 shift() 中间件:

¥To solve this problem, we have the shift() middleware:

import {
  computePosition,
  flip,
  shift,
} from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.6.3/+esm';
 
const button = document.querySelector('#button');
const tooltip = document.querySelector('#tooltip');
 
computePosition(button, tooltip, {
  placement: 'top',
  middleware: [flip(), shift()],
}).then(({x, y}) => {
  Object.assign(tooltip.style, {
    left: `${x}px`,
    top: `${y}px`,
  });
});
My tooltip with more content

现在,我们可以阅读所有文本,因为 shift() 中间件将工具提示从底部居中的位置移动到完全可见。

¥Now, we can read all the text because the shift() middleware shifted the tooltip from its bottom centered placement until it was fully in view.

正如你所看到的,工具提示与文档边界的边缘完全齐平。要添加一些空格或填充,shift() 中间件接受一个选项对象:

¥As you can see, the tooltip lies fully flush with the edge of the document boundary. To add some whitespace, or padding, the shift() middleware accepts an options object:

import {
  computePosition,
  flip,
  shift,
} from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.6.3/+esm';
 
const button = document.querySelector('#button');
const tooltip = document.querySelector('#tooltip');
 
computePosition(button, tooltip, {
  placement: 'top',
  middleware: [flip(), shift({padding: 5})],
}).then(({x, y}) => {
  Object.assign(tooltip.style, {
    left: `${x}px`,
    top: `${y}px`,
  });
});
My tooltip with more content

现在,工具提示和边界边缘之间有 5px 的喘息空间。

¥Now there’s 5px of breathing room between the tooltip and the edge of the boundary.

偏移中间件

¥Offset middleware

我们可能也不希望工具提示与按钮元素齐平。为此,我们有 offset() 中间件:

¥We probably also don’t want the tooltip to lie flush with the button element. For this, we have the offset() middleware:

import {
  computePosition,
  flip,
  shift,
  offset,
} from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.6.3/+esm';
 
const button = document.querySelector('#button');
const tooltip = document.querySelector('#tooltip');
 
computePosition(button, tooltip, {
  placement: 'top',
  middleware: [offset(6), flip(), shift({padding: 5})],
}).then(({x, y}) => {
  Object.assign(tooltip.style, {
    left: `${x}px`,
    top: `${y}px`,
  });
});

这会将工具提示从参考元素中移出 6px:

¥This will displace the tooltip 6px from the reference element:

My tooltip with more content

箭头中间件

¥Arrow middleware

大多数工具提示都有一个指向按钮的箭头(或三角形/插入符号)。为此,我们有 arrow() 中间件,但我们首先需要在工具提示中添加一个新元素:

¥Most tooltips have an arrow (or triangle / caret) which points toward the button. For this, we have the arrow() middleware, but we first need to add a new element inside of our tooltip:

<div id="tooltip" role="tooltip">
  My tooltip with more content
  <div id="arrow"></div>
</div>

然后设置样式:

¥Then style it:

#arrow {
  position: absolute;
  background: #222;
  width: 8px;
  height: 8px;
  transform: rotate(45deg);
}

然后将箭头元素传递到 arrow() 中间件中:

¥Then pass the arrow element into the arrow() middleware:

import {
  computePosition,
  flip,
  shift,
  offset,
  arrow,
} from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.6.3/+esm';
 
const button = document.querySelector('#button');
const tooltip = document.querySelector('#tooltip');
const arrowElement = document.querySelector('#arrow');
 
computePosition(button, tooltip, {
  placement: 'top',
  middleware: [
    offset(6),
    flip(),
    shift({padding: 5}),
    arrow({element: arrowElement}),
  ],
}).then(({x, y}) => {
  Object.assign(tooltip.style, {
    left: `${x}px`,
    top: `${y}px`,
  });
});

现在我们需要向箭头元素添加动态样式。与其他中间件不同,arrow() 中间件不会修改主 xy 坐标。相反,它提供数据供我们使用。我们可以通过 middlewareData 获取此信息。

¥Now we need to add dynamic styles to the arrow element. Unlike other middleware, the arrow() middleware doesn’t modify the main x and y coordinates. Instead, it provides data for us to use. We can access this piece of information provided via middlewareData.

其中包含一个 arrow 对象,引用我们使用的 arrow() 中间件的名称:

¥This contains an arrow object, referring to the name of the arrow() middleware we used:

computePosition(button, tooltip, {
  placement: 'top',
  middleware: [
    offset(6),
    flip(),
    shift({padding: 5}),
    arrow({element: arrowElement}),
  ],
}).then(({x, y, placement, middlewareData}) => {
  Object.assign(tooltip.style, {
    left: `${x}px`,
    top: `${y}px`,
  });
 
  // Accessing the data
  const {x: arrowX, y: arrowY} = middlewareData.arrow;
});

我们现在想要使用这些数据来应用样式。

¥We now want to use this data to apply the styles.

computePosition(button, tooltip, {
  placement: 'top',
  middleware: [
    offset(6),
    flip(),
    shift({padding: 5}),
    arrow({element: arrowElement}),
  ],
}).then(({x, y, placement, middlewareData}) => {
  Object.assign(tooltip.style, {
    left: `${x}px`,
    top: `${y}px`,
  });
 
  // Accessing the data
  const {x: arrowX, y: arrowY} = middlewareData.arrow;
 
  const staticSide = {
    top: 'bottom',
    right: 'left',
    bottom: 'top',
    left: 'right',
  }[placement.split('-')[0]];
 
  Object.assign(arrowElement.style, {
    left: arrowX != null ? `${arrowX}px` : '',
    top: arrowY != null ? `${arrowY}px` : '',
    right: '',
    bottom: '',
    [staticSide]: '-4px',
  });
});

上面的样式将处理所有展示位置的箭头位置。

¥The styles above will handle the arrow’s position for all placements.

  • x 是 x 轴偏移,仅在垂直放置('top''bottom')时才存在。

    ¥x is the x-axis offset, only existing if the placement is vertical ('top' or 'bottom').

  • y 是 y 轴偏移,仅在水平放置('right''left')时才存在。

    ¥y is the y-axis offset, only existing if the placement is horizontal ('right' or 'left').

staticSide 取决于选择的 placement。例如,如果位置是 'bottom',那么我们希望箭头位于工具提示顶部之外 4px 处(因此我们使用负数)。

¥staticSide depends on the placement that gets chosen. For instance, if the placement is 'bottom', then we want the arrow to be positioned 4px outside of the top of the tooltip (so we use a negative number).

我们使用 .split('-')[0] 的原因是还可以处理对齐的位置,例如 'top-start' 而不仅仅是 'top'

¥The reason we use .split('-')[0] is to also handle aligned placements, like 'top-start' rather than only 'top'.

My tooltip with more content

功能性

¥Functionality

现在工具提示已定位所有内容,我们可以添加用户交互,以便在悬停或聚焦按钮时显示工具提示。

¥Now that the tooltip has everything positioned, we can add user interactions that will show the tooltip when hovering or focusing the button.

#tooltip {
  display: none;
  width: max-content;
  position: absolute;
  top: 0;
  left: 0;
  background: #222;
  color: white;
  font-weight: bold;
  padding: 5px;
  border-radius: 4px;
  font-size: 90%;
}
function update() {
  computePosition(button, tooltip, {
    // ... options ...
  }).then(({x, y, placement, middlewareData}) => {
    // ... positioning logic ...
  });
}
 
function showTooltip() {
  tooltip.style.display = 'block';
  update();
}
 
function hideTooltip() {
  tooltip.style.display = '';
}
 
[
  ['mouseenter', showTooltip],
  ['mouseleave', hideTooltip],
  ['focus', showTooltip],
  ['blur', hideTooltip],
].forEach(([event, listener]) => {
  button.addEventListener(event, listener);
});

尝试将鼠标悬停或聚焦按钮:

¥Try hovering or focusing the button:

杂项

¥Misc

要在滚动或调整大小时保持工具提示锚定到按钮,你需要使用 autoUpdate 实用程序。

¥To keep the tooltip anchored to the button while scrolling or resizing, you’ll want to use the autoUpdate utility.

至于动画,这取决于你在制作浮动元素时探索!

¥As for animations, this is up to you to explore when crafting your floating elements!

完成

¥Complete

你现在已经使用 Floating UI 创建了一个基本的可访问工具提示。

¥You’ve now created a basic accessible tooltip using Floating UI.