Popper Logo
Popper Logo

Modifiers

Popper is built using an extensible core, which provides the foundation used to deliver all the functionalities offered by the library.

All the useful functionalities provided by the library are implemented as Popper modifiers. They are plugins, or middlewares, that can hook into the lifecycle of Popper, and add additional logic to the positioning operations provided by default by Popper. They effectively "modify" the popper state in some fashion, adding functionality, hence the term "modifiers".

Don't mind tech-related ads? Consider disabling your ad-blocker to help us!
They are small and unobtrusive.
Alternatively, consider to support us on Open Collective!

Custom Modifiers

Is it possible to add custom modifiers, written by you, by defining them in the options.modifiers array during a Popper instantation.

Example modifier

A modifier that logs to the console when it's on the top (not very useful, but demonstrates each property):

const topLogger = {
  name: 'topLogger',
  enabled: true,
  phase: 'main',
  fn({ state }) {
    if (state.placement === 'top') {
      console.log('Popper is on the top');
    }
  },
};

createPopper(reference, popper, {
  modifiers: [topLogger],
});

A modifier is composed of an object with the following properties:

type Modifier = {|
  // Required properties
  name: string,
  enabled: boolean,
  phase: ModifierPhases,
  fn: (ModifierArguments<Options>) => ?State,

  // Optional properties
  requires?: Array<string>,
  requiresIfExists?: Array<string>,
  effect?: (ModifierArguments<Options>) => ?() => void,
  options?: {},
  data?: {},
|};

type ModifierArguments<Options: Obj> = {
  state: $Shape<State>,
  instance: Instance,
  options: $Shape<Options>,
  name: string,
};

name

The name is used as an identifier to make it possible to refer to the modifier from other parts of the library. For example, you can add an object to the options.modifier array with the name property, and the options property populated with some custom options, to override the options of a built-in modifier.

createPopper(reference, popper, {
  modifiers: [
    {
      name: 'flip',
      options: {
        fallbackPlacements: ['top', 'bottom'],
      },
    },
  ],
});

enabled

If set to true, the modifier will be executed during the Popper lifecycle, otherwise, it will be ignored.

phase

Popper's modifiers lifecycle is divided into 3 core phases: read, main, and write. This is done to optimize the library so that its access to the DOM is grouped together rather than scattered around the whole lifecycle.

The modifiers that need to read from the DOM should run in the read phase, the ones that only perform logic with algorithms should live in main, and the ones that write to the DOM should be under write.

Note that Popper provides a cache of DOM measurements in its state, so that modifiers can read them rather than querying the DOM, optimizing the overall execution time. This means you should rarely need to hook into the read phase.

For further granularity if needed, there are 2 other sub-phases: before and after. Here is the full list:

  • beforeRead
  • read
  • afterRead
  • beforeMain
  • main
  • afterMain
  • beforeWrite
  • write
  • afterWrite

fn

This is the main function, used to provide the logic to the modifier.

There are cases when you may want to control the Popper lifecycle through a modifier – for example the flip modifier can alter the placement option, and if that happens, Popper is instructed to run all the modifiers again, so that they can react to the updated placement value. A modifier can reset the lifecycle by setting state.reset to true.

requires

Specifies the list of modifiers it depends upon. Popper will execute the modifiers in the correct order to allow the dependent modifier to access the data provided by the dependee modifier.

For example, offset relies on popperOffsets data, since it mutates its properties.

In short, the modifier depends upon this list of modifiers' data to work.

requiresIfExists

Specifies the list of modifiers it depends upon, but only if those modifiers actually exist.

For example, preventOverflow relies on offset, but only if offset actually exists, because offset mutates the popperOffsets data, which preventOverflow needs to read and mutate. If offset doesn't exist, preventOverflow will still work as normal.

In short, the modifier depends upon this list of modifiers' behavior (or mutations to dependent data from other modifiers) to work.

effect

This function allows you to execute arbitrary code (effects) before the first update cycle is ran. Perform effects in the function and return a cleanup function if necessary:

function effect() {
  // perform side effects
  return () => {
    // cleanup side effects
  };
}

Examples where this is useful involve code that is not necessary to be run on every update, rather only when the instance lifecycle changes (created, updated, or destroyed):

  • The eventListeners modifier uses this to add and remove window/document event listeners which is not part of the main modifier update cycle.
  • The arrow modifier uses this to add the element to state.elements. The arrow modifier is dependent on preventOverflow (run after), but preventOverflow depends on state.elements.arrow. Since effects are run before the first update cycle, the problem is resolved.

options

This is an object with all the properties used to configure the modifier.

data

This is the initial data provided to state.modifiersData.<MODIFIER_NAME>, which is shared to other modifiers to be read or manipulated.

Edit this page
Lifecycle
© 2020 MIT License