Created
August 16, 2025 12:35
-
-
Save kotarok/bc336e2fe05d69cb24586cb6a84cdbd7 to your computer and use it in GitHub Desktop.
Observe elements entering/leaving viewport and apply classes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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