Slider example

How it works

A carousel from a list of slides. Items per breakpoint on the HTML, controls and autoplay in the config.

Slider turns the children of a wrapper into a carousel: a strip of slides that moves sideways, with dots or prev/next buttons to drive it, touch on mobile and an optional autoplay. It runs on tiny-slider, a dependency-free library that builds its own outer markup, nav and controls around the container. The class in src/js/handler/slider only hides the wrapper while tiny-slider builds it and wires one callback.

How many slides are visible is decided on the HTML, with data-slider-items and one attribute per breakpoint of the foundation. Everything else (dots, buttons, autoplay) is a tiny-slider option, and each combination is a config in Handler.js mapped to its own selector: js--slider, js--slider-x and js--slider-y on this page.

The markup

A wrapper with the hook and the items attributes; every direct child is one slide. The outer c--slider-a is only there to style what tiny-slider injects (the .tns-nav dots) and to hold the custom controls when there are any.

<!-- The wrapper is the tiny-slider container; every child is one slide -->
<div class="c--slider-a">
  <div class="c--slider-a__wrapper js--slider" data-slider-items="2">
    <div class="c--slider-a__wrapper__item">1</div>
    <div class="c--slider-a__wrapper__item">2</div>
    <div class="c--slider-a__wrapper__item">3</div>
  </div>
</div>

The handler

Every config starts from commonConfig. It reads the items attributes into tiny-slider's items and responsive options, sets dots at the bottom and no buttons, and adds two callbacks: onInit runs a GSAP tween to full opacity on the wrapper, and onSlideTransitionEnd revalidates the Lazy instance of the page, so images that were off screen in a hidden slide are loaded when the slide comes into view.

this.commonConfig = ({ element }) => {
    const responsive = {};

    const addBreakpoint = (width, attr) => {
        const value = parseInt(element.getAttribute(attr), 10);
        if (!isNaN(value)) {
            responsive[width] = { items: value };
        }
    };

    addBreakpoint(580, "data-slider-items-tablets");
    addBreakpoint(810, "data-slider-items-tabletm");
    addBreakpoint(1024, "data-slider-items-tabletl");
    addBreakpoint(1300, "data-slider-items-laptop");
    addBreakpoint(1570, "data-slider-items-desktop");
    addBreakpoint(1700, "data-slider-items-wide");

    return {
        onInit: () => {
            var gsap = this.Manager.getLibrary("GSAP").gsap;
            gsap.to(element, { opacity: 1, duration: 0.5, ease: "power4.in", delay: 0.25 });
        },
        onSlideTransitionEnd: () => {
            const lazyInstances = this.Manager.getInstances("Lazy");
            if (lazyInstances && lazyInstances.length > 0) {
                lazyInstances[0].instance.revalidate();
            }
        },
        // Items can come from an attribute or be fixed at 2
        items: parseInt(element.getAttribute("data-slider-items")) || 2,
        controls: false,
        nav: true,
        navPosition: "bottom",
        responsive,
    };
};

The default config passes the common one as it is. The two variations spread it and override a few keys; they are shown in their blocks below. Note the shape: the class receives sliderConfig and passes it to tns() with the wrapper as container, keeping onSlideTransitionEnd for itself since it is not a tiny-slider option.

this.config = ({ element }) => {
    return {
        Manager: this.Manager,
        sliderConfig: this.commonConfig({ element }),
    };
};

What the framework does for you

  • Lazy loading. A slider is only built when it gets close to the viewport (Boostify, scroll method): within 30px for the default and custom-controls groups, 50px for the autoplay one. tiny-slider is imported once and cached by the Manager.
  • Page transitions. Every instance is destroyed before swup replaces the content, which removes the markup tiny-slider injected, and rebuilt on the new page.
  • Cross-handler events. Slider is registered with modifyHeight: true in resources.js, and the handler listens for Slider:load and Slider:destroy. AnchorTo fires the load event for every height-modifying library before it scrolls, so a slider between the trigger and the destination is built first and the anchor lands in the right place; the chapter strip under the hero does it on this page. Modal fires it when its slider variation injects a js--slider into the dialog, and the destroy event on close. The load listener queries the page again and assigns with forceLoad: true, which skips the viewport check:
this.emitter.on("Slider:load", (payload) => {
    this.DOM = this.updateTheDOM;

    super.assignInstances({
        elementGroups: [
            {
                elements: this.DOM.sliderElement,
                config: this.config,
                boostify: { distance: 30 },
            },
        ],
        forceLoad: true,
    });
});

Default

Two slides on screen at every width, dots underneath, no buttons.

Default items="2"

The simplest case: js--slider and a fixed number of items. Six numbered slides, two visible, and the dots below to move between them. On touch devices the strip can be swiped. With no breakpoint attributes the count is the same at every width, which only works when the slides are simple enough to fit two on a phone.

<div class="c--slider-a">
  <div class="c--slider-a__wrapper js--slider" data-slider-items="2">
    <div class="c--slider-a__wrapper__item"><p class="f--font-c">1</p></div>
    <div class="c--slider-a__wrapper__item"><p class="f--font-c">2</p></div>
    …
  </div>
</div>
this.config = ({ element }) => {
    return {
        Manager: this.Manager,
        sliderConfig: this.commonConfig({ element }),
    };
};
Live example
items="2"

1

2

3

4

5

6

Items per breakpoint

Three slides on small screens, four from 1024px. Same config, one more attribute.

Attributes items="3"items-tabletl="4"

Each data-slider-items-<breakpoint> attribute adds an entry to tiny-slider's responsive option, keyed by the pixel width of that breakpoint in the foundation. tiny-slider treats them like min-width media queries: the base data-slider-items applies from 0, and each breakpoint inherits from the smaller ones, so you only name the widths where the count changes.

Here the count goes from 3 to 4 at 1024px (tabletl). Resize the window to see the fourth slide appear. With six slides and four on screen there are still two more to scroll to; if a breakpoint ever asks for as many items as there are slides, tiny-slider freezes the slider and hides the dots, since there is nothing to move.

<!-- 3 items up to 1023px, 4 from 1024px (tabletl) -->
<div class="c--slider-a">
  <div class="c--slider-a__wrapper js--slider" data-slider-items="3" data-slider-items-tabletl="4">
    <div class="c--slider-a__wrapper__item"><p class="f--font-d">1</p></div>
    <div class="c--slider-a__wrapper__item"><p class="f--font-d">2</p></div>
    …
  </div>
</div>
Live example
items="3"items-tabletl="4"

1

2

3

4

5

6

Custom controls

Your own prev and next buttons instead of the dots. One slide on mobile, two from 810px, four from 1300px.

Variation nav: falsecontrols: truecontrolsContainer

configVariationX, on js--slider-x. nav: false removes the dots and controls: true turns the buttons on, but instead of letting tiny-slider inject its own, controlsContainer points at an element you write: the handler passes element.nextElementSibling, so the controls block has to come right after the wrapper. tiny-slider takes its first child as the previous button and its second as the next one, and keeps their aria-* and disabled state up to date. The buttons are styled by you (c--slider-a__ft__btn here); js--slider-controls is a hook for your own code, the handler does not read it.

This one also sets three breakpoints: one slide on a phone, two from tabletm (810px) and four from laptop (1300px).

<!-- The controls must be the next sibling of the wrapper: the handler passes element.nextElementSibling -->
<div class="c--slider-a">
  <div class="c--slider-a__wrapper js--slider-x" data-slider-items="1" data-slider-items-tabletm="2" data-slider-items-laptop="4">
    <div class="c--slider-a__wrapper__item"><p class="f--font-b">1</p></div>
    <div class="c--slider-a__wrapper__item"><p class="f--font-b">2</p></div>
    …
  </div>
  <div class="c--slider-a__ft js--slider-controls">
    <button class="c--slider-a__ft__btn" type="button">Previous slide</button>
    <button class="c--slider-a__ft__btn" type="button">Next slide</button>
  </div>
</div>
this.configVariationX = ({ element }) => {
    const common = this.commonConfig({ element });
    return {
        Manager: this.Manager,
        sliderConfig: {
            // we spread the object returned by our common config to add it to our custom config
            ...common,
            // We add custom controls to our slider
            nav: false,
            controls: true,
            controlsContainer: element.nextElementSibling,
        },
    };
};
Live example
nav: falsecontrols: truecontrolsContainer

1

2

3

4

5

6

Autoplay

The slides advance on their own every five seconds. Dots on top, your buttons below, no start/stop button.

Variation autoplay: trueautoplayButtonOutput: falsenavPosition: "top"controlsContainer

configVariationY, on js--slider-y. autoplay: true moves to the next slide every 5000ms, the tiny-slider default, and keeps looping; the dots and the buttons still work in between. tiny-slider normally injects a start/stop button next to the slides; autoplayButtonOutput: false leaves it out. The dots keep their default state from the common config but move above the strip with navPosition: "top", and the same controlsContainer as the variation above adds the two buttons underneath, so this slider has both ways to drive it.

Autoplay pauses when the page tab is hidden and resumes when it is visible again (autoplayResetOnVisibility, on by default). The instance also exposes play() and pause(), which other handlers can call.

<div class="c--slider-a">
  <div class="c--slider-a__wrapper js--slider-y" data-slider-items="2">
    <div class="c--slider-a__wrapper__item"><p class="f--font-a">Mara</p></div>
    <div class="c--slider-a__wrapper__item"><p class="f--font-a">Gotzon</p></div>
    …
  </div>
  <div class="c--slider-a__ft js--slider-controls">
    <button class="c--slider-a__ft__btn" type="button">Previous member</button>
    <button class="c--slider-a__ft__btn" type="button">Next member</button>
  </div>
</div>
this.configVariationY = ({ element }) => {
    const common = this.commonConfig({ element });
    return {
        Manager: this.Manager,
        sliderConfig: {
            ...common,
            controls: true,
            autoplay: true,
            autoplayButtonOutput: false,
            navPosition: "top",
            controlsContainer: element.nextElementSibling,
        },
    };
};
Live example
autoplay: trueautoplayButtonOutput: falsenavPosition: "top"controlsContainer

Mara

Gotzon

Nerea

Eli

Felipe

Amaia

Diego

Andres

Angela

Mauro

Options reference

The attributes the handler reads from the wrapper, and the tiny-slider options the configs set.

Reference

Attributes (on the wrapper)

AttributeFromWhat it does
data-slider-items0px, default 2Slides visible at once. Becomes tiny-slider's items
data-slider-items-tablets580pxSlides visible from this width up
data-slider-items-tabletm810pxSlides visible from this width up
data-slider-items-tabletl1024pxSlides visible from this width up
data-slider-items-laptop1300pxSlides visible from this width up
data-slider-items-desktop1570pxSlides visible from this width up
data-slider-items-wide1700pxSlides visible from this width up

The breakpoint attributes fill tiny-slider's responsive option, which works like min-width: a width you do not name inherits the count of the one below it. A value that is not a number is ignored.

tiny-slider options set by the configs

OptionValuesWhat it does
itemsnumber, from data-slider-itemsSlides visible at once. If there are as many slides or fewer, the slider is not initialised
responsive{ 1024: { items: 4 }, … }, from the breakpoint attributesOptions per min-width
navtrue (common), false (custom controls)The dots, one per page of slides
navPosition"bottom" (common), "top" (autoplay)Where the dots go, relative to the slides
controlsfalse (common), true (both variations)Prev and next buttons. With a focused button the arrow keys move the slider too
controlsContainerelement.nextElementSibling (both variations)An element with at least two children, used as the prev and next buttons instead of the injected ones
autoplaytrue (autoplay)Advances on its own every autoplayTimeout ms, 5000 by default
autoplayButtonOutputfalse (autoplay)Does not inject the start/stop button
onInitfunction (common)Called by tiny-slider once built. The handler tweens the wrapper to full opacity with GSAP
onSlideTransitionEndfunction (common)Not a tiny-slider option: the class binds it to the transitionEnd event. The handler revalidates Lazy there

Anything else (gutter, loop, speed, mouseDrag, autoHeight, vertical axis, gallery mode…) is a tiny-slider option that goes in sliderConfig: see the options reference. To add a combination, write a new config in Handler.js and map it to its own js-- selector in updateTheDOM and elementGroups, like the two variations. The dots and buttons are styled in c--slider-a, which is an example of styling, not part of the handler.