FezVrasta/popper.js

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

Summary

Maintainability
Test Coverage
<PageCard className="mb-8">

# flip

Changes the placement of the floating element to keep it in view.

<MiddlewareContainer>
  <MiddlewareBadge type="Visibility Optimizer" />
</MiddlewareContainer>

<ShowFor packages={['core']}>

```js
import {flip} from '@floating-ui/core';
```

</ShowFor>

<ShowFor packages={['dom']}>

```js
import {flip} from '@floating-ui/dom';
```

</ShowFor>

<ShowFor packages={['react']}>

```js
import {flip} from '@floating-ui/react';
```

</ShowFor>

<ShowFor packages={['react-dom']}>

```js
import {flip} from '@floating-ui/react-dom';
```

</ShowFor>

<ShowFor packages={['vue']}>

```js
import {flip} from '@floating-ui/vue';
```

</ShowFor>

<ShowFor packages={['react-native']}>

```js
import {flip} from '@floating-ui/react-native';
```

</ShowFor>

This prevents the floating element from overflowing along its
side axis by flipping it to the opposite side by default.

</PageCard>

<div className="flex flex-col gap-4">
  <Chrome
    label="Scroll up"
    center
    scrollable="y"
    className="h-96"
  >
    <Floating
      middleware={[
        {name: 'flip', options: {rootBoundary: 'document'}},
      ]}
    >
      <div className="grid h-32 w-32 place-items-center border-2 border-dashed border-gray-1000" />
    </Floating>
  </Chrome>
</div>

## Usage

<ShowFor packages={['core', 'dom']}>

```js
computePosition(referenceEl, floatingEl, {
  middleware: [flip()],
});
```

</ShowFor>

<ShowFor packages={['react', 'react-dom', 'react-native']}>

```js
useFloating({
  middleware: [flip()],
});
```

</ShowFor>

<ShowFor packages={['vue']}>

```js
useFloating(reference, floating, {
  middleware: [flip()],
});
```

</ShowFor>

<Notice type="warning" title="Note">
  This is an alternative to `autoPlacement(){:js}`, so only one of either can be used.
  See [how they differ here](/docs/flip#conflict-with-autoplacement).
</Notice>

## Options

These are the options you can pass to `flip(){:js}`.

```ts
interface FlipOptions extends DetectOverflowOptions {
  mainAxis?: boolean;
  crossAxis?: boolean;
  fallbackAxisSideDirection?: 'none' | 'start' | 'end';
  flipAlignment?: boolean;
  fallbackPlacements?: Array<Placement>;
  fallbackStrategy?: 'bestFit' | 'initialPlacement';
}
```

### `mainAxis{:.key}`

default: `true{:js}`

This is the main axis in which overflow is checked to perform a
flip. By disabling this, it will ignore overflow.

- `y`-axis for `'top'{:js}` and `'bottom'{:js}` placements
- `x`-axis for `'left'{:js}` and `'right'{:js}` placements

```js
flip({
  mainAxis: false,
});
```

<div className="flex flex-col gap-4">
  <Chrome
    label="Scroll up"
    center
    scrollable="y"
    className="h-96"
  >
    <Floating
      content="I will ignore main axis overflow"
      middleware={[
        {
          name: 'flip',
          options: {
            mainAxis: false,
          },
        },
      ]}
    >
      <div className="grid h-32 w-32 place-items-center border-2 border-dashed border-gray-1000" />
    </Floating>
  </Chrome>
</div>

### `crossAxis{:.key}`

default: `true{:js}`

This is the cross axis in which overflow is checked to perform a
flip, the axis perpendicular to `mainAxis{:.key}`. By disabling
this, it will ignore overflow.

```js
flip({
  crossAxis: false,
});
```

<div className="flex flex-col gap-4">
  <Chrome
    label="Scroll around"
    center
    scrollable="both"
    className="h-96"
  >
    <Floating
      placement="bottom-start"
      content="I will check cross axis overflow (default)"
      middleware={[
        {
          name: 'flip',
          options: {
            crossAxis: true,
            rootBoundary: 'document'
          },
        },
      ]}
    >
      <div className="grid place-items-center w-32 h-32 border-2 border-gray-1000 border-dashed" />
    </Floating>
  </Chrome>

  <Chrome
    label="Scroll around"
    center
    scrollable="both"
    className="h-96"
  >
    <Floating
      placement="bottom-start"
      content="I will ignore cross axis overflow"
      middleware={[
        {
          name: 'flip',
          options: {
            crossAxis: false,
            rootBoundary: 'document'
          },
        },
      ]}
    >
      <div className="grid place-items-center w-32 h-32 border-2 border-gray-1000 border-dashed" />
    </Floating>
  </Chrome>
</div>

### `fallbackAxisSideDirection{:.key}`

default: `'none'{:js}`

Whether to allow fallback to the opposite axis if no placements
along the preferred placement axis fit, and if so, which side
direction along that axis to choose. If necessary, it will
fallback to the other direction.

- `'none'{:js}` signals that no fallback to the opposite axis
  should take place.
- `'start'{:js}` represents `'top'{:js}` or `'left'{:js}`.
- `'end'{:js}` represents `'bottom'{:js}` or `'right'{:js}`.

<Notice gap="above">
  In RTL writing direction, the x-axis directions are reversed.
</Notice>

For instance, by default, if the initial `placement{:.key}` is
set to `'right'{:js}`, then the placements to try (in order) are:

`['right', 'left']{:js}`

On a narrow viewport, it's possible or even likely that _neither_
of these will fit.

By specifying a string other than `'none'{:js}`, you allow
placements along the perpendicular axis of the initial placement
to be tried. The direction determines which side of placement is
tried first:

```js
flip({
  fallbackAxisSideDirection: 'start',
});
```

The above results in: `['right', 'left', 'top', 'bottom']{:js}`.

```js
flip({
  fallbackAxisSideDirection: 'end',
});
```

The above results in: `['right', 'left', 'bottom', 'top']{:js}`.

As an example, if you'd like a tooltip that has a placement of
`'right'{:js}` to be placed on top on mobile (assuming it doesn't
fit), then you'd use `'start'{:js}`. For an interactive popover,
you likely want to use `'end'{:js}` so it's placed on the bottom,
closer to the user's fingers.

In each of the following demos, the `placement{:.key}` is
`'right'{:js}`.

<div className="flex flex-col gap-4">
  <Chrome
    label="Scroll horizontally"
    center
    scrollable="x"
    className="h-96"
  >
    <Floating
      placement="right"
      content="`fallbackAxisSideDirection` has been set to 'none' (default)"
      middleware={[
        {
          name: 'flip',
          options: {
            fallbackAxisSideDirection: 'none',
            rootBoundary: 'document'
          },
        },
      ]}
    >
      <div className="grid place-items-center w-32 h-32 border-2 border-gray-1000 border-dashed mt-12" />
    </Floating>
    <p>Notice that it can overflow.</p>
  </Chrome>

<Chrome
  label="Scroll horizontally"
  center
  scrollable="x"
  className="h-96"
>
  <Floating
    placement="right"
    content="`fallbackAxisSideDirection` has been set to 'start'"
    middleware={[
      {
        name: 'flip',
        options: {
          fallbackAxisSideDirection: 'start',
          rootBoundary: 'document',
        },
      },
      {
        name: 'shift',
      },
    ]}
  >
    <div className="mt-12 grid h-32 w-32 place-items-center border-2 border-dashed border-gray-1000" />
  </Floating>
  <p>Notice that it prefers `top` if it doesn't fit.</p>
</Chrome>

  <Chrome
    label="Scroll horizontally"
    center
    scrollable="x"
    className="h-96"
  >
    <Floating
      placement="right"
      content="`fallbackAxisSideDirection` has been set to 'end'"
      middleware={[
        {
          name: 'flip',
          options: {
            fallbackAxisSideDirection: 'end',
            rootBoundary: 'document'
          },
        },
        {
          name: 'shift'
        }
      ]}
    >
      <div className="grid place-items-center w-32 h-32 border-2 border-gray-1000 border-dashed mt-12" />
    </Floating>
    <p>Notice that it prefers `bottom` if it doesn't fit.</p>
  </Chrome>
</div>

<Notice title="Recommended reading" gap="above">

Check [combining with `shift`](/docs/flip#combining-with-shift)
when using this option.

</Notice>

### `flipAlignment{:.key}`

default: `true{:js}`

When an alignment is specified, e.g. `'top-start'{:js}` instead
of just `'top'{:js}`, this will flip to `'top-end'{:js}` if
`start{:.string}` doesn't fit.

```js
flip({
  flipAlignment: false,
});
```

When using this with the `shift(){:js}` middleware, ensure
`flip(){:js}` is placed **before** `shift(){:js}` in your
middleware array. This ensures the `flipAlignment{:.key}` logic
can act before `shift(){:js}`'s does.

### `fallbackPlacements{:.key}`

default: `[oppositePlacement]{:js}`

This describes an **explicit** array of placements to try if the
initial `placement{:.key}` doesn't fit on the axes in which
overflow is checked.

```js
flip({
  fallbackPlacements: ['right', 'bottom'],
});
```

In the above example, if `placement{:.key}` is set to
`'top'{:js}`, then the placements to try (in order) are:

`['top', 'right', 'bottom']{:js}`

<div className="flex flex-col gap-4">
  <Chrome
    label="Scroll down"
    center
    scrollable="y"
    className="h-96"
  >
    <Floating
      placement="top"
      content="Floating"
      middleware={[
        {
          name: 'flip',
          options: {
            fallbackPlacements: ['right', 'bottom'],
            rootBoundary: 'document',
          },
        },
      ]}
    >
      <div className="grid h-32 w-32 place-items-center border-2 border-dashed border-gray-1000" />
    </Floating>
  </Chrome>
</div>

<Notice gap="above">
  The options `flipAlignment{:.key}` and
  `fallbackAxisSideDirection{:.key}` no longer have an effect if
  this option is explicitly specified, as they are only shortcuts to
  create a computed list of fallback placements. To ensure your
  explicit list is preferred, they will be ignored.
</Notice>

<Notice title="Recommended reading" gap="above">

Check [combining with `shift`](/docs/flip#combining-with-shift)
when using this option.

</Notice>

### `fallbackStrategy{:.key}`

default: `'bestFit'{:js}`

When no placements fit, then you'll want to decide what happens.
`'bestFit'{:js}` will use the placement which fits best on the
checked axes. `'initialPlacement'{:js}` will use the initial
`placement{:.key}` specified.

```js
flip({
  fallbackStrategy: 'initialPlacement',
});
```

### ...detectOverflowOptions

All of [`detectOverflow`](/docs/detectOverflow#options)'s options
can be passed. For instance:

```js
flip({
  padding: 5, // 0 by default
});
```

### Deriving options from state

You can derive the options from the
[middleware lifecycle state](/docs/middleware#middlewarestate):

```js
flip((state) => ({
  padding: state.rects.reference.width,
}));
```

## Final placement

The placement returned from the function is always the final one,
not necessarily the one you passed in as the "preferred" one.

<ShowFor packages={['core', 'dom']}>

```js
computePosition(referenceEl, floatingEl, {
  placement: 'bottom',
  middleware: [flip()],
}).then(({placement}) => {
  console.log(placement); // 'top' or 'bottom'
});
```

</ShowFor>

<ShowFor packages={['react', 'react-dom', 'react-native']}>

```js
// placement can be 'top' or 'bottom'
const {placement} = useFloating({
  placement: 'bottom',
  middleware: [flip()],
});
```

</ShowFor>

<ShowFor packages={['vue']}>

```js
// placement can be 'top' or 'bottom'
const {placement} = useFloating(reference, floating, {
  placement: 'bottom',
  middleware: [flip()],
});
```

</ShowFor>

## Combining with `shift(){:js}`

If you're allowing fallback to the opposite axis of the preferred
placement via `fallbackAxisSideDirection{:.key}` or
`fallbackPlacements{:.key}`, then you may want to determine what
behavior you want when combining `flip(){:js}` with
`shift(){:js}`.

If you want to ensure the placement side is preserved as best as
possible without flipping, then you have two options to consider:

1. Disabling the cross axis check in `flip(){:js}` places top
   priority on `shift(){:js}` to do its work, with `flip(){:js}`
   only taking action when absolutely necessary. The preferred
   placement of the floating element will be conserved as much as
   possible.

```js {4}
const middleware = [
  flip({
    fallbackAxisSideDirection: 'start',
    crossAxis: false,
  }),
  shift(),
];
```

2. Placing `shift(){:js}` _before_ `flip(){:js}` in the array
   ensures it can do its work before `flip(){:js}` tries to
   change the placement. This is similar to the first technique
   above, but allows `flip(){:js}` to take action on its
   `crossAxis{:.key}` too -- useful if `shift(){:js}` has a
   limiter.

```js {3}
const middleware = [
  shift({
    limiter: limitShift(),
  }),
  flip({
    fallbackAxisSideDirection: 'start',
  }),
];
```

<Notice>

The second method is only recommended if your placement is not
edge aligned, e.g. `'top'{:js}` instead of `'top-start'{:js}`.
This is because alignment flipping will no longer be possible
since `shift` takes over first.

For reusable/generic components, you can use basic conditional
checks on the supplied preferred placement to alter the ordering.

</Notice>

## Conflict with `autoPlacement(){:js}`

`flip(){:js}` and `autoPlacement(){:js}` cannot be used together
inside the same middleware array; make sure you choose only one
of them to use.

The reason is they both try to perform work on the placement but
with opposing strategies. Therefore, they will continually try to
change the result or work of the other one, leading to a reset
loop.

- `flip(){:js}` uses a fallback "no space" strategy. Ensures the
  preferred placement is kept unless there is no space left.
- `autoPlacement(){:js}` uses a primary "most space" strategy.
  Always chooses the placement with the most space available.