dequelabs / dequelabs/axe-core

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

Open
#5,069 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
7.5k
Forks
933
Avg merge
2d 13h
Merged PRs (30d)
29

Description

## 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:

```js
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:

```html



Visible until scrolled



```

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:

```js
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 ``, 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)

Contributor guide

Open the contributing guide

Research direction

Start at the dom.isOffscreen entry point and its noParentScrolled helper, then trace the existing visibility checks for vertical and horizontal coordinates. Verify behavior for absolute, static/relative, and fixed elements in scrolled containers; done means scrolled content is not treated as hidden while intentionally offscreen and fixed elements remain independent of ancestor scrolling.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
accessibility
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.