webdevpuneet.com

Floating UI Anchored Popover in a Scroll Container — Free Snippet

Floating UI Anchored Popover in a Scroll Container · Modals · Plain HTML, CSS & JS · Live preview

CategoryModals

Copy & paste

Source Code

<div class="ap-wrap">
  <div class="ap-hint">Each row's info button opens a popover anchored exactly to it</div>
  <div class="ap-scroll" id="apScroll">
    <div class="ap-row" data-name="Ada Chen" data-role="Engineering" data-note="Joined 2023, leads the platform team.">
      <span>Ada Chen</span><button class="ap-info" type="button" aria-label="Info">i</button>
    </div>
    <div class="ap-row" data-name="Marco Reyes" data-role="Design" data-note="Joined 2024, owns the design system.">
      <span>Marco Reyes</span><button class="ap-info" type="button" aria-label="Info">i</button>
    </div>
    <div class="ap-row" data-name="Priya Nair" data-role="Product" data-note="Joined 2022, manages the roadmap.">
      <span>Priya Nair</span><button class="ap-info" type="button" aria-label="Info">i</button>
    </div>
    <div class="ap-row" data-name="Tom Baker" data-role="Support" data-note="Joined 2025, on the support rotation.">
      <span>Tom Baker</span><button class="ap-info" type="button" aria-label="Info">i</button>
    </div>
    <div class="ap-row" data-name="Sofia Costa" data-role="Marketing" data-note="Joined 2021, runs campaigns.">
      <span>Sofia Costa</span><button class="ap-info" type="button" aria-label="Info">i</button>
    </div>
    <div class="ap-row" data-name="Liam Park" data-role="Engineering" data-note="Joined 2024, works on infra.">
      <span>Liam Park</span><button class="ap-info" type="button" aria-label="Info">i</button>
    </div>
  </div>
  <div class="ap-pop" id="apPop">
    <div class="ap-pop-name" id="apPopName"></div>
    <div class="ap-pop-role" id="apPopRole"></div>
    <div class="ap-pop-note" id="apPopNote"></div>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Single shared popover element
One DOM node reused and repositioned per click, not one per row.
Fresh autoUpdate per anchor
Old scroll-tracking is torn down before a new one starts.
Live scroll-following
The popover tracks its row in real time, not just at open.
Unified close path
Toggle-off and outside-click both call the same cleanup function.
Edge-aware placement
flip and shift keep the popover visible near list boundaries.
Data-driven content
Popover text comes straight from each row's own attributes.

About this UI Snippet

Floating UI Anchored Popover in a Scroll Container — One Popover, Many Anchors

Screenshot of the Floating UI Anchored Popover in a Scroll Container snippet rendered live

A list where every row has its own "info" button doesn't need a separate popover element per row — it needs one shared popover that gets repositioned and refilled with different content depending on which button was clicked, and that stays correctly pinned to that specific button even while the list scrolls underneath it.

One popover element, reused for every row

Rather than creating six popovers (one per row) and toggling their visibility, there's exactly one #apPop element in the DOM. openFor(btn) reads that row's data-* attributes into the popover's content and repositions the single shared element against whichever button was actually clicked — simpler markup, and no risk of two popovers ever being visible at once by accident.

autoUpdate is re-established on every open, against the new anchor

Each call to openFor first cleans up any previous autoUpdate subscription, then starts a fresh one anchored to the *newly clicked* button. This matters because the previous popover's tracked position was relative to a different button entirely — reusing a stale autoUpdate subscription would keep recalculating position against the wrong anchor.

Scrolling doesn't require re-opening the popover

Because autoUpdate is actively watching the scroll container (it listens for scroll events on all scrollable ancestors of the anchor, not just window scroll), scrolling the list while a popover is open keeps the popover correctly glued to its row's button in real time — a version that computed position once at open time would leave the popover visually detached from its row the moment the list scrolls even slightly.

Toggling and outside-click both route through one close function

Clicking the same info button again, and clicking anywhere outside both the list and the popover, both call the identical closePop() — which hides the popover, clears the currentAnchor tracking variable, and tears down the autoUpdate subscription. There's only one way the popover actually closes, so its cleanup can never be partially applied.

placement: 'left-start' plus flip/shift handles a list near the edge

The popover opens to the left of the info button by default, but the same flip/shift middleware from the auto-flipping tooltip pattern elsewhere in this collection applies here too — a button near the container's top or bottom edge still gets a correctly positioned, fully visible popover.

Reusing it

This one-shared-element-with-live-repositioning pattern applies to any list of per-row popovers, dropdowns, or context menus — a comments list, a table of records, a file browser — swap the row data and popover content for whatever each item actually needs to show.

Step by step

How to Use

  1. 1
    Add the Floating UI CDNLoad @floating-ui/core, then @floating-ui/dom, in that order.
  2. 2
    Paste HTML, CSS, and JSA scrollable list of six people renders with info buttons.
  3. 3
    Click any info buttonA popover opens anchored exactly to that row.
  4. 4
    Scroll the listThe popover stays correctly pinned to its row's button.
  5. 5
    Click a different info buttonThe popover moves and updates to that row's info.
  6. 6
    Click outside the listThe popover closes.

Real-world uses

Common Use Cases

People directory details
Show a per-person popover from each row's info button, using one shared popover element refilled with that person's details on every click.
Data table row details
Expand compact info icons into small cards, with autoUpdate keeping the popover pinned to its own row as the table scrolls.
File browser metadata
Pair with the auto-flipping tooltip so popovers flip sides when a row sits near the edge of the container.
Activity list context
Reveal expandable context for notifications and events, with a unified close path shared by toggle-off and outside-click.
Floating UI at scale
Learn how to tear down the previous scroll tracking before starting a fresh autoUpdate, which avoids leaks in long lists of anchors.

Got questions?

Frequently Asked Questions

A single shared popover element that gets repositioned and refilled with different content is simpler to manage than six separate popover elements, and it structurally guarantees only one can ever be visible at a time — there's no way for two popovers to accidentally both be open at once, since there's only one popover element in the entire page to show or hide.

Each autoUpdate subscription continuously recalculates the popover's position specifically relative to the anchor element it was created with. If a new row's button is clicked while an old autoUpdate subscription (tracking a different, previous button) is still running, the position would keep being calculated against the wrong anchor. Calling the stored cleanup function before starting a fresh autoUpdate against the newly clicked button ensures only one active position-tracking subscription exists at a time, always matching the currently open popover's real anchor.

autoUpdate doesn't only watch window-level scroll and resize events — it specifically detects and listens to scroll events on all scrollable ancestor elements of the anchor, which includes this snippet's scrollable list container. That's what lets the popover continuously recompute and update its position as the list is scrolled, keeping it visually pinned to its row's button rather than drifting away from it.

Clicking the currently active button again calls closePop(), fully hiding the popover and tearing down its position tracking — a toggle-off. Clicking a different button calls openFor() with the new button, which updates the popover's content, repositions it against the new anchor, and replaces the autoUpdate subscription — the popover doesn't close and reopen visually, it moves and updates in place.

The same single-popover, click-to-open-for-that-row pattern scales to any number of rows without changes, since exactly one popover element and one autoUpdate subscription exist regardless of how many rows are in the list — only the row data (and how you read it, whether from data attributes as here or from a JavaScript data array) needs to scale with your actual dataset.