FezVrasta/popper.js

View on GitHub
website/pages/docs/tutorial.mdx

Summary

Maintainability
Test Coverage
import * as T from '../../lib/components/Tutorial';

<PageCard>

# Tutorial

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

</PageCard>

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](/docs/computePosition).

## Before proceeding

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

Create a new HTML document with two elements, a `<button>{:html}`
and a tooltip `<div>{:html}`:

```html
<!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):

<T.Result1 />

## Styling

Let's give our tooltip some styling:

```html
<!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:

<T.Result2 />

## Making the tooltip "float"

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

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.

```css {2-5}
#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](/docs/computePosition#initial-layout) to
learn more.

<T.Result3 />

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

Inside your module `<script>{:html}` tag, add the following code:

[Play on CodeSandbox](https://codesandbox.io/s/floating-ui-dom-template-nzvvx?file=/src/index.js)

```js
import {computePosition} from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@__DOM_VERSION__/+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`,
  });
});
```

Above, we call `computePosition(){:js}` with the button and
tooltip elements as arguments.

It returns a `Promise{:.class}`, so we use the `.then(){:js}`
method which passes in the calculated `x{:.param}` and
`y{:.param}` coordinates for us, which we use to assign
`left{:.key}` and `top{:.key}` styles to the tooltip.

Our tooltip is now centered underneath the button:

<T.Result4 />

## Placements

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

```js {7}
import {computePosition} from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@__DOM_VERSION__/+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`,
  });
});
```

<T.Result5 />

The available base placements are `'top'{:js}`, `'right'{:js}`,
`'bottom'{:js}`, `'left'{:js}`.

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

## Our first problem

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'{:js}` placement?

<T.Result6 />

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'{:js}` placement, instead of `'top'{:js}`. 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.

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.

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

`flip(){:js}` modifies the coordinates for us such that it uses
the `'bottom'{:js}` placement automatically without us needing to
explicitly specify it.

```js /flip/
import {
  computePosition,
  flip,
} from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@__DOM_VERSION__/+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`,
  });
});
```

Now, even though we've set `placement{:.key}` to `'top'{:js}`,
the tooltip flips to the bottom automatically for us if it can't
fit on the top. No need to adjust anything manually.

<T.Result7 />

Importantly, it will continue to use the `'top'{:js}` placement
at all times if it can, and only fallback to `'bottom'{:js}` if
it has to.

### Shift middleware

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

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

<T.Result8 />

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.

To solve this problem, we have the `shift(){:js}` middleware:

```js /shift/
import {
  computePosition,
  flip,
  shift,
} from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@__DOM_VERSION__/+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`,
  });
});
```

<T.Result9 />

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

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

```js /padding/
import {
  computePosition,
  flip,
  shift,
} from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@__DOM_VERSION__/+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`,
  });
});
```

<T.Result10 />

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

### Offset middleware

We probably also don't want the tooltip to lie flush with the
button element. For this, we have the `offset(){:js}` middleware:

```js /offset/
import {
  computePosition,
  flip,
  shift,
  offset,
} from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@__DOM_VERSION__/+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`,
  });
});
```

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

<T.Result11 />

<Notice>
  Notice that we put `offset(){:js}` at the start of the array,
  rather than at the end? Middleware behavior is dependent on
  **order**. Each middleware returns new coordinates which
  middleware later in the array will use. The `offset(){:js}`
  middleware is one that should be before most other ones,
  because they should modify their coordinates based on the
  offset ones.
</Notice>

### Arrow middleware

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

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

Then style it:

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

Then pass the arrow element into the `arrow(){:js}` middleware:

```js {6,11,19}
import {
  computePosition,
  flip,
  shift,
  offset,
  arrow,
} from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@__DOM_VERSION__/+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`,
  });
});
```

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

This contains an `arrow` object, referring to the name of the
`arrow(){:js}` middleware we used:

```js {9,15-16}
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.

```js {18-31}
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` is the x-axis offset, only existing if the placement is
  vertical (`'top'{:js}` or `'bottom'{:js}`).
- `y` is the y-axis offset, only existing if the placement is
  horizontal (`'right'{:js}` or `'left'{:js}`).

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

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

<T.Result12 />

## Functionality

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

```css {2}
#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%;
}
```

```js
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:

<T.Result13 />

## Misc

To keep the tooltip anchored to the button while scrolling or
resizing, you'll want to use the [`autoUpdate`](/docs/autoUpdate)
utility.

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

## Complete

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