Skip to content

isOffscreen incorrectly handles scrolled elements, especially with position: absolute and horizontal scroll #5069

Description

@WilcoFiers

Problem

dom.isOffscreen is used to determine if an element is visually hidden by being placed outside the viewport. It has a helper function noParentScrolled that attempts to distinguish between elements that are intentionally positioned offscreen (e.g. left: -9999px) and elements that are simply scrolled out of view inside a scroll container. There are several issues with how this works today.

Issue 1: position: absolute elements always treated as offscreen when above the viewport

In the "above viewport" check:

if (
  coords.bottom <= 0 &&
  (noParentScrolled(domNode, coords.bottom) || styl.position === 'absolute')
) {
  return true;
}

The || styl.position === 'absolute' condition bypasses the scroll-parent check entirely. This assumes that an absolute element above the viewport must be intentionally hidden. But an absolute element is positioned relative to its nearest positioned ancestor — if that ancestor is inside a scroll container, the absolute element scrolls with it. For example:

<div style="overflow: scroll; height: 200px">
  <div style="position: relative; height: 1000px">
    <div style="position: absolute; top: 0">Visible until scrolled</div>
  </div>
</div>

When the scroll container is scrolled down, the absolute element moves above the viewport. isOffscreen incorrectly reports it as offscreen (hidden), which causes isVisible / isVisibleOnScreen to treat it as invisible.

Issue 2: No horizontal scroll consideration

noParentScrolled only checks scrollTop (vertical scrolling). The horizontal offscreen checks don't use noParentScrolled at all:

if (dir === 'ltr') {
  return coords.right <= 0;
}

If an element is inside a horizontally scrollable container and has been scrolled to the left so that coords.right <= 0, isOffscreen reports it as offscreen with no scroll-container check to guard against it.

Issue 3: noParentScrolled doesn't account for CSS positioning contexts

The function walks the entire ancestor tree up to <html>, accumulating scrollTop. It doesn't consider how positioning contexts affect which scroll containers are relevant:

  • For static/relative elements, any ancestor scroll container can scroll the element.
  • For absolute elements, only scroll containers at or above the containing block (nearest positioned ancestor) are relevant.
  • For fixed elements, no ancestor scroll containers are relevant — fixed elements are positioned relative to the viewport.

Currently, noParentScrolled doesn't distinguish these cases. This can cause false negatives for fixed elements: if the page is scrolled down, noParentScrolled finds a scrollTop on an ancestor and returns false ("a parent is scrolled"), preventing a truly offscreen fixed element from being detected.

Impact

isOffscreen is used by both isVisible and isVisibleOnScreen. False positives (reporting scrolled content as offscreen) cause those elements to be treated as invisible, which can lead to:

  • Rules skipping elements that are actually visible and interactive
  • Color contrast checks not running on content that is visible when scrolled into view
  • target-size and other checks not flagging visible elements

Suggested approach

Rather than accumulating scrollTop/scrollLeft up the tree, check whether the element's layout position (ignoring scroll) falls within the scrollable range of its ancestor scroll containers. If it does, the element is "in the page" even if temporarily scrolled out of view.

The fix should:

  1. Remove the position: absolute shortcut
  2. Add horizontal scroll-container awareness
  3. Account for CSS positioning contexts when determining which scroll containers are relevant
  4. Handle position: fixed elements separately (they are unaffected by ancestor scroll)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions