Carousel (Images)

Scroll through a set of slides with lazy-loaded images: only visible and adjacent slides load.

Live Demo

Watch the Network tab: images only request when they enter the visible window or its adjacent buffer.

Source

HTML + JavaScript
<div data-component="my-carousel" data-cloak>
    <button data-action="prev" data-bind-attr="{ disabled: !canPrev }">‹</button>
    <div class="viewport" data-bind-style="{ width: viewportWidth }">
        <div class="track" data-bind-style="{ transform: trackTransform }" data-list="enrichedItems">
            <template>
                <div class="slide">
                    <img data-show="loaded" data-bind-attr="{ src: url, alt: label }">
                    <div data-show="!loaded">Loading…</div>
                    <div data-bind="label"></div>
                </div>
            </template>
        </div>
    </div>
    <button data-action="next" data-bind-attr="{ disabled: !canNext }">›</button>
</div>

<script>
wildflower.component('my-carousel', {
    state: {
        offset: 0,
        visibleCount: 3,
        slideWidth: 160,
        gap: 12,
        items: [
            { url: 'https://picsum.photos/id/10/320/240', label: 'Slide 1' },
            { url: 'https://picsum.photos/id/20/320/240', label: 'Slide 2' },
            { url: 'https://picsum.photos/id/30/320/240', label: 'Slide 3' },
            { url: 'https://picsum.photos/id/40/320/240', label: 'Slide 4' },
            { url: 'https://picsum.photos/id/50/320/240', label: 'Slide 5' },
            { url: 'https://picsum.photos/id/60/320/240', label: 'Slide 6' }
        ]
    },
    computed: {
        maxOffset() { return Math.max(0, this.items.length - this.visibleCount); },
        canPrev()   { return this.offset > 0; },
        canNext()   { return this.offset < this.maxOffset; },
        viewportWidth() {
            const n = this.visibleCount;
            return (this.slideWidth * n + this.gap * (n - 1)) + 'px';
        },
        trackTransform() {
            return 'translateX(-' + (this.offset * (this.slideWidth + this.gap)) + 'px)';
        },
        // Mark each slide 'loaded' if it's in the viewport OR one slot beyond each edge.
        // The src binding only resolves to a real URL when loaded is true, so off-screen
        // images never request.
        enrichedItems() {
            const start = this.offset - 1;
            const end = this.offset + this.visibleCount;
            return this.items.map((item, i) => ({
                ...item,
                loaded: i >= start && i <= end
            }));
        }
    },
    prev() { if (this.canPrev) this.offset--; },
    next() { if (this.canNext) this.offset++; }
});
</script>

Key Points

  • enrichedItems is a computed array that augments each item with a loaded flag derived from offset. When offset changes, the flags recompute and images render in/out reactively
  • data-show="loaded" on the <img> keeps the element hidden until it's time to load; the placeholder fills the slot in the meantime
  • data-bind-attr binds the image src reactively; an image outside the loaded window never has a src set, so the browser never fetches it
  • The adjacent buffer (offset - 1 and offset + visibleCount) pre-fetches one slide beyond each edge, so images are ready before they slide into view
  • trackTransform is a computed CSS transform; WF's reactive style binding handles the slide animation without manual DOM writes
  • The viewport is pinned to an exact multiple of slideWidth + gap so no partial slide peeks in