Skip to content

Instantly share code, notes, and snippets.

@kotarok
Created August 16, 2025 12:35
Show Gist options
  • Select an option

  • Save kotarok/bc336e2fe05d69cb24586cb6a84cdbd7 to your computer and use it in GitHub Desktop.

Select an option

Save kotarok/bc336e2fe05d69cb24586cb6a84cdbd7 to your computer and use it in GitHub Desktop.
Observe elements entering/leaving viewport and apply classes
/**
* Observe elements entering/leaving viewport and apply classes
* @param {string} selector - CSS selector
* @param {Object} [options] - Configuration object
* @param {Element} [options.scope=document] - Element to search within
* @param {Element} [options.root=null] - Root element for intersection (null = viewport)
* @param {string} [options.inView] - Class to add when element enters view
* @param {string} [options.outView] - Class to add when element leaves view (requires toggle: true)
* @param {Function} [options.onEnter] - Callback when element enters view
* @param {Function} [options.onLeave] - Callback when element leaves view
* @param {boolean} [options.toggle=false] - Toggle classes on enter/leave
* @param {string} [options.offset='0px'] - Trigger offset ('100px', '50%', '-200px')
* @param {number} [options.threshold=0] - Intersection threshold (0.0-1.0)
* @param {number} [options.delay=0] - Delay before applying changes (ms)
* @returns {ViewObserver} ViewObserver instance for control
* @example
* // Simple usage
* observeInView('.item', { inView: 'visible' })
*
* // Full configuration
* const observer = observeInView('.cards', {
* inView: 'active',
* outView: 'inactive',
* onEnter: el => console.log('Entered'),
* toggle: true,
* threshold: 0.5,
* scope: container
* })
*
* // Control
* observer.stop()
*/
const observeInView = (selector, options = {}) => {
const observer = new ViewObserver(selector, options)
observer.start()
return observer
}
class ViewObserver {
constructor(
selector,
{
scope = document,
root = null,
inView,
outView,
onEnter,
onLeave,
toggle = false,
offset = '0px',
threshold = 0,
delay = 0,
} = {}
) {
this.selector = selector
this.scope = scope
this.root = root
this.visibleClass = inView
this.invisibleClass = outView
this.enterCallback = onEnter
this.leaveCallback = onLeave
this.toggle = toggle
this.offset = offset
this.threshold = threshold
this.delay = delay
this.observer = null
}
start() {
const targets = this.scope.querySelectorAll(this.selector)
if (!targets.length) return false
const handleEntry = (entry) => {
const { target, isIntersecting } = entry
if (isIntersecting) {
if (this.visibleClass) target.classList.add(this.visibleClass)
if (this.invisibleClass) target.classList.remove(this.invisibleClass)
this.enterCallback?.(target, entry)
if (!this.toggle) this.observer.unobserve(target)
} else if (this.toggle) {
if (this.visibleClass) target.classList.remove(this.visibleClass)
if (this.invisibleClass) target.classList.add(this.invisibleClass)
this.leaveCallback?.(target, entry)
}
}
this.observer = new IntersectionObserver(
(entries) =>
entries.forEach((entry) =>
this.delay > 0 ? setTimeout(() => handleEntry(entry), this.delay) : handleEntry(entry)
),
{ root: this.root, rootMargin: this.offset, threshold: this.threshold }
)
targets.forEach((target) => {
// Set initial state for invisible class
if (this.invisibleClass) {
if (this.root) {
// For custom root, check intersection with root element
const targetRect = target.getBoundingClientRect()
const rootRect = this.root.getBoundingClientRect()
const isInitiallyVisible = !(
targetRect.bottom < rootRect.top ||
targetRect.top > rootRect.bottom ||
targetRect.right < rootRect.left ||
targetRect.left > rootRect.right
)
if (!isInitiallyVisible) {
target.classList.add(this.invisibleClass)
}
} else {
// For viewport, use existing logic
const { top, bottom } = target.getBoundingClientRect()
if (!(top < window.innerHeight && bottom > 0)) {
target.classList.add(this.invisibleClass)
}
}
}
this.observer.observe(target)
})
return this.observer
}
stop() {
this.observer?.disconnect()
return this
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment