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:
- Remove the
position: absolute shortcut
- Add horizontal scroll-container awareness
- Account for CSS positioning contexts when determining which scroll containers are relevant
- Handle
position: fixed elements separately (they are unaffected by ancestor scroll)
Problem
dom.isOffscreenis used to determine if an element is visually hidden by being placed outside the viewport. It has a helper functionnoParentScrolledthat 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: absoluteelements always treated as offscreen when above the viewportIn the "above viewport" check:
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:When the scroll container is scrolled down, the absolute element moves above the viewport.
isOffscreenincorrectly reports it as offscreen (hidden), which causesisVisible/isVisibleOnScreento treat it as invisible.Issue 2: No horizontal scroll consideration
noParentScrolledonly checksscrollTop(vertical scrolling). The horizontal offscreen checks don't usenoParentScrolledat all:If an element is inside a horizontally scrollable container and has been scrolled to the left so that
coords.right <= 0,isOffscreenreports it as offscreen with no scroll-container check to guard against it.Issue 3:
noParentScrolleddoesn't account for CSS positioning contextsThe function walks the entire ancestor tree up to
<html>, accumulatingscrollTop. It doesn't consider how positioning contexts affect which scroll containers are relevant:static/relativeelements, any ancestor scroll container can scroll the element.absoluteelements, only scroll containers at or above the containing block (nearest positioned ancestor) are relevant.fixedelements, no ancestor scroll containers are relevant — fixed elements are positioned relative to the viewport.Currently,
noParentScrolleddoesn't distinguish these cases. This can cause false negatives forfixedelements: if the page is scrolled down,noParentScrolledfinds ascrollTopon an ancestor and returnsfalse("a parent is scrolled"), preventing a truly offscreenfixedelement from being detected.Impact
isOffscreenis used by bothisVisibleandisVisibleOnScreen. False positives (reporting scrolled content as offscreen) cause those elements to be treated as invisible, which can lead to:target-sizeand other checks not flagging visible elementsSuggested approach
Rather than accumulating
scrollTop/scrollLeftup 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:
position: absoluteshortcutposition: fixedelements separately (they are unaffected by ancestor scroll)