Refresh teleported dropdown size & position when base size or position changes

Author: gtiersmaCreated Jun 22, 2026Updated Jun 22, 2026

When using the use-teleport prop, the dropdown is teleported to a higher-level DOM element (<body/> by default) and is correctly positioned where it should be, adjacent to the base vue-multiselect textbox.

However, if anything causes the size of the vue-multiselect textbox to change (container changing size, focusing the textbox with tags, selecting/removing tags in a way that changes the number of lines of tags) or if anything causes the position of the textbox to change (scrolling), the teleported dropdown stays where it is with the same size & position as before, causing it to appear to disconnect from the textbox in a buggy manner:

Image

Externally working around the problem

This is what I've been doing in my own code (external to vue-multiselect), in case it helps anyone.

The process involves visually hiding the dropdown the moment resizing/repositioning begins, then re-showing the dropdown (if possible) once it ends.

(As a disclaimer, please note that these code snippets are shared in a manner that refactoring into your own solution is necessary and intentional. In other words, they won't compile the way they are here if you just copy & paste them into a TS file.)


To begin with, forcing the isOpen boolean within vue-multiselect to change can be used to cause the watcher to activate to reposition and resize the dropdown to where it should be:

  multiselect: MultiselectComponent

  /**
   * This seems to be the only way to reposition the dropdown through vue-multiselect.
   *
   * Essentially, it just forces the "isOpen" watcher internal to the dependency to go off. It handles positioning it.
   *
   * The only drawback is that the dropdown seems to "flicker" each time this is called,
   * but there's probably nothing that can be done about that at this time.
   */
  private repositionDropdown() {
    this.multiselect.isOpen = false
    nextTick(() => this.multiselect.isOpen = true)
  }

A ResizeObserver is used for size changes:

this.sizeObserver = new ResizeObserver(() => this.repositionDropdown())

// ...on open:
this.sizeObserver.observe(this.multiselect.$el)

// ...on close:
this.sizeObserver.disconnect()

An IntersectionObserver is used for tracking with a boolean whether the vue-multiselect textbox has been scrolled into overflow:


  /**
   * Is set to "true" whenever the dropdown is shown and starts to partially become hidden
   * due to scrolling causing the dropdown to go into overflow.
   */
  private isDropdownInOverflow: boolean

    // The unusual threshold ensures it checks in cases where the vue-multiselect textbox is a few px in/out of overflow.
    this.overflowObserver = new IntersectionObserver(
      entries => this.handleOverflowDetection(entries),
      { threshold: [0, 0.03, 0.97, 1] }
    )

/**
   * Calculates whether the dropdown is now being "disconnected" from the textbox
   * due to vue-multiselect entering/exiting overflow in a scrollable container.
   *
   * Math.round must be used because in some cases the numbers are fractional pixels that may not be an exact match
   * when they should be considered close enough.
   */
  private handleOverflowDetection(entries: IntersectionObserverEntry[]) {
    this.isDropdownInOverflow = entries.some(it =>
      Math.round(it.intersectionRect.bottom) !== Math.round(it.boundingClientRect.bottom)
    )
  }

// on open:
this.overflowObserver.observe(this.multiselect.$el)

// on close:
this.overflowObserver.disconnect()

Finally, resize and global scroll listeners to hide the dropdown while the user is performing the action, then re-show it when done:


  /**
   * Class applied to the dropdown container whenever the user is doing something that causes the vue-multiselect to move around.
   * Used to apply specific styling to the dropdown whenever the action occurs.
   */
  private readonly MOVEMENT_CLASS: string = "moving"

  /**
   * The container DOM element that holds the teleported dropdown element.
   */
  private readonly dropdownContainer: HTMLElement

// on open:
window.addEventListener('scroll', this.hideForEvent, true)
window.addEventListener('resize', this.hideForEvent)

// on close:
window.removeEventListener('scroll', this.hideForEvent, true)
window.removeEventListener('resize', this.hideForEvent)
this.dropdownContainer.classList.remove(this.MOVEMENT_CLASS)

  /**
   * Meant to be throttled/debounced & called repeatedly from an event listener.
   *
   * Applies the "moving" class to the dropdown as soon as it starts to get called, then removes it when the calling stops.
   */
  private hideForEvent(event: Event) {

    // Skip scroll events originating from vue-multiselect:
    if (event.type === 'scroll') {
      const scrollingEl = event.target as HTMLElement
      if (this.dropdownContainer.contains(scrollingEl) || this.multiselect.$el.contains(scrollingEl)) {
        return
      }
    }

    this.dropdownContainer.classList.add(this.MOVEMENT_CLASS)
    this.showForEvent()
  }

  /**
   * May need to be debounced/throttled.
   *
   * Removes the "moving" class from the dropdown as long as vue-multiselect is still visible.
   * Otherwise, it's removed when the vue-multiselect gets visible.
   *
   * Once the moving class is removed, the dropdown is repositioned to where it should be.
   */
  private showForEvent() {
    if (!this.isDropdownInOverflow) {
      this.repositionDropdown()
      this.dropdownContainer.classList.remove(this.MOVEMENT_CLASS)
    }
  }

  // In CSS:
  .moving .multiselect__content-wrapper {
    visibility: hidden;
  }

It's recommended to use debouncing/throttling with some (if not all) of these observers and events.