Here’s how to check if an element is visible in the viewport after scrolling in JavaScript, including modern and legacy approaches:
1. Using getBoundingClientRect()
(Legacy Method)
Checks if the element is at least partially visible in the viewport.
function isElementVisible(el) {
const rect = el.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
// Check vertical and horizontal visibility
const vertInView = rect.top <= windowHeight && rect.bottom >= 0;
const horInView = rect.left <= windowWidth && rect.right >= 0;
return vertInView && horInView;
}
Usage:
window.addEventListener('scroll', () => {
const element = document.querySelector('#myElement');
if (isElementVisible(element)) {
console.log('Element is visible!');
}
});
2. Check Full Visibility
Verify if the entire element is visible:
function isElementFullyVisible(el) {
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
3. Intersection Observer API (Modern Method)
Efficiently detect visibility changes without constant scroll-event checks:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('Element is visible!', entry.target);
}
});
}, {
threshold: 0.1 // Trigger when 10% of the element is visible
});
// Start observing an element
const element = document.querySelector('#myElement');
observer.observe(element);
Options:
threshold
: 0.0 (any visibility) to 1.0 (fully visible)rootMargin
: Adjust the detection boundary (e.g., “100px” to trigger earlier)
4. Combined Visibility Check
Ensure the element isn’t hidden by CSS and is in the viewport:
function isElementTrulyVisible(el) {
// Check CSS visibility
const style = window.getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
return false;
}
// Check viewport position
const rect = el.getBoundingClientRect();
return (
rect.top < window.innerHeight && rect.bottom > 0 &&
rect.left < window.innerWidth && rect.right > 0
);
}
5. Performance Optimization
Throttle Scroll Events
Avoid excessive checks during rapid scrolling:
function throttle(func, limit = 200) {
let lastCall;
return function() {
const now = Date.now();
if (!lastCall || (now - lastCall) >= limit) {
func.apply(this, arguments);
lastCall = now;
}
};
}
window.addEventListener('scroll', throttle(() => {
if (isElementVisible(element)) {
// Handle visibility
}
}));
Key Scenarios
Case | getBoundingClientRect | IntersectionObserver |
---|---|---|
Partial Visibility | ✅ | ✅ (configurable) |
Full Visibility | ✅ | ✅ |
Performance | Needs throttling | Built-in optimization |
Hidden via CSS | ❌ (requires extra checks) | ❌ (same) |
When to Use Which
- Legacy Approach: Support older browsers (IE9+) or need fine-grained control.
- Intersection Observer: Modern apps, better performance, complex visibility rules.
- Combined Checks: Critical UIs where hidden elements should never trigger visibility.
Example Workflow
- Use
IntersectionObserver
for scroll-driven content (e.g., lazy-load images). - Use
getBoundingClientRect
for one-time checks or legacy support. - Always pair with CSS visibility checks if elements might be hidden.