@terrahq/lazy

Lightweight lazy loading built on IntersectionObserver. Zero dependencies.

1. Images

Scroll down to load images
Demo 1 Demo 2 Demo 3
More images below
Demo 4 Demo 5 Demo 6
<img class="g--lazy-01" data-src="photo.jpg" alt="..." />
import Lazy from '@terrahq/lazy';

const lazy = new Lazy();
.g--lazy-01 {
    opacity: 0;
    transition: opacity 0.4s ease;
}
.g--lazy-01--is-active {
    opacity: 1;
}

2. Background Images

Scroll down for backgrounds
<!-- Any non-img/video/iframe element uses background-image -->
<div class="g--lazy-01 hero" data-src="hero.jpg"></div>

// Lazy preloads with new Image(), then applies:
// element.style.backgroundImage = url("hero.jpg")
import Lazy from '@terrahq/lazy';

const lazy = new Lazy();

3. Videos

Scroll down for videos
<!-- Direct src -->
<video class="g--lazy-01" data-src="clip.mp4" muted autoplay loop playsinline></video>

<!-- Multiple sources -->
<video class="g--lazy-01" muted autoplay loop playsinline>
    <source data-src="clip.webm" type="video/webm" />
    <source data-src="clip.mp4" type="video/mp4" />
</video>
import Lazy from '@terrahq/lazy';

const lazy = new Lazy();

4. Iframes

Scroll down for iframes
<iframe class="g--lazy-01" data-src="https://www.youtube.com/embed/VIDEO_ID"></iframe>
import Lazy from '@terrahq/lazy';

const lazy = new Lazy();

5. Manual Trigger (click to load)

Manual image
Manual image 2
<button id="btn">Load image</button>
<img id="hero" class="g--lazy-01" data-src="hero.jpg" alt="..." />
import Lazy from '@terrahq/lazy';

// Create instance then immediately destroy observer
const lazy = new Lazy({
    selector: '.g--lazy-01',
});
lazy.destroy(); // no auto-loading

// Load on click
document.querySelector('#btn').addEventListener('click', () => {
    lazy.load(document.querySelector('#hero'));
});

6. Infinite Marquee + Lazy

Scroll down for marquee
Marquee 1
Marquee 2
Marquee 3
Marquee 4
Marquee 5
Marquee 6
Marquee 7
Marquee 8

Infinite Marquee Reversed + Lazy

Marquee Rev 1
Marquee Rev 2
Marquee Rev 3
Marquee Rev 4
Marquee Rev 5
Marquee Rev 6
<div class="marquee" id="marquee">
    <div class="marquee-item">
        <img class="g--lazy-01" data-src="image.jpg" alt="..." />
    </div>
    <!-- more items... -->
</div>
import Lazy from '@terrahq/lazy';
import gsap from 'gsap';
import { horizontalLoop } from '@andresclua/infinite-marquee-gsap';

const lazy = new Lazy();

const el = document.querySelector('#marquee');
const loop = horizontalLoop(el.children, {
    paused: false,
    repeat: -1,
    speed: 1,
});

// Pause on hover
el.addEventListener('mouseenter', () => {
    gsap.to(loop, { timeScale: 0, overwrite: true });
});
el.addEventListener('mouseleave', () => {
    gsap.to(loop, { timeScale: 1, overwrite: true });
});

// IntersectionObserver picks up items as GSAP
// transforms move them into the viewport. No revalidate() needed.
.marquee {
    display: flex;
    overflow: hidden;
    height: 250px;
}
.marquee-item {
    flex-shrink: 0;
    width: 350px;
    height: 100%;
}

7. Tiny Slider + Lazy

Scroll down for slider
Slide 1
Slide 2
Slide 3
Slide 4
Slide 5
Slide 6
Slide 8
<div id="slider">
    <div class="slider-item">
        <img class="g--lazy-01" data-src="slide.jpg" alt="..." />
    </div>
    <!-- more slides... -->
</div>
import Lazy from '@terrahq/lazy';
import { tns } from 'tiny-slider';

const lazy = new Lazy();

const slider = tns({
    container: '#slider',
    items: 3,
    loop: false,
});

// After each slide transition, revalidate so newly
// visible slides get observed and loaded
slider.events.on('transitionEnd', () => {
    lazy.revalidate();
});

8. Error Handling

Error test
const lazy = new Lazy({
    onError: (el) => {
        console.warn('Failed to load:', el);
    },
    onComplete: () => {
        console.log('All elements processed (success + errors)');
    },
});
/* Added automatically via errorClass */
.g--lazy-01--is-error {
    opacity: 1;
    outline: 2px solid red;
}