I'm playing around with IntersectionObserver
in two different scrolling containers: one uses a 3D perspective transform, and the other uses a simpler 2D transform.
For the 3D container, the observer callback fires constantly while scrolling, even when the red box remains fully visible. Meanwhile, in the 2D container, it behaves how I'd expect—only firing noticeably when entering or leaving the viewport portion.
Below is my minimal example. If you open it and scroll each container separately, you’ll see the 3D container logs frequent small intersection changes, whereas the 2D container logs fewer. Is this normal with 3D transforms? Or am I misusing IntersectionObserver
?
You can run the example(opening it on the full page so you can see it nicely)scroll and find the difference between these two.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>IntersectionObserver 3D vs 2D Transform</title>
<style>
pare-section {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.container {
width: 300px;
height: 200px;
border: 2px solid #333;
overflow-y: scroll;
position: relative;
}
.content {
height: 600px;
background: linear-gradient(to bottom, #eee, #ddd);
}
/* 3D container with perspective */
.transform-container-3d {
perspective: 500px;
}
.content3d {
transform: rotateX(15deg);
transform-origin: top center;
}
/* 2D container with a simple rotation */
.transform-container-2d .content2d {
transform: rotate(5deg);
transform-origin: top center;
}
.target-box {
width: 100px;
height: 100px;
margin: 50px auto;
background-color: red;
}
.logs {
font-family: monospace;
white-space: pre;
border: 1px solid #ccc;
padding: 10px;
height: 150px;
width: 270px;
overflow: auto;
}
h2 {
margin: 0 0 10px;
}
</style>
</head>
<body>
<h1>IntersectionObserver 3D vs 2D Transform Test</h1>
<div class="compare-section">
<div class="container transform-container-3d">
<h2>3D Container</h2>
<div class="content content3d">
<div class="target-box" id="target3d"></div>
</div>
</div>
<div class="container transform-container-2d">
<h2>2D Container</h2>
<div class="content content2d">
<div class="target-box" id="target2d"></div>
</div>
</div>
</div>
<div class="compare-section">
<div class="logs" id="log3d">3D Logs: </div>
<div class="logs" id="log2d">2D Logs: </div>
</div>
<script>
const target3d = document.getElementById('target3d');
const log3d = document.getElementById('log3d');
const target2d = document.getElementById('target2d');
const log2d = document.getElementById('log2d');
const observer3d = new IntersectionObserver((entries) => {
entries.forEach(entry => {
log3d.textContent += `ratio: ${entry.intersectionRatio.toFixed(2)}\n`;
});
}, {
root: document.querySelector('.transform-container-3d'),
threshold: [0, 0.25, 0.5, 0.75, 1]
});
const observer2d = new IntersectionObserver((entries) => {
entries.forEach(entry => {
log2d.textContent += `ratio: ${entry.intersectionRatio.toFixed(2)}\n`;
});
}, {
root: document.querySelector('.transform-container-2d'),
threshold: [0, 0.25, 0.5, 0.75, 1]
});
observer3d.observe(target3d);
observer2d.observe(target2d);
</script>
</body>
</html>