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: trueinresources.js, and the handler listens forSlider:loadandSlider: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 ajs--sliderinto the dialog, and the destroy event on close. The load listener queries the page again and assigns withforceLoad: 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.
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 }),
};
}; items="2" Items per breakpoint
Three slides on small screens, four from 1024px. Same config, one more attribute.
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> items="3"items-tabletl="4" Custom controls
Your own prev and next buttons instead of the dots. One slide on mobile, two from 810px, four from 1300px.
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,
},
};
}; nav: falsecontrols: truecontrolsContainer Autoplay
The slides advance on their own every five seconds. Dots on top, your buttons below, no start/stop button.
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,
},
};
}; autoplay: trueautoplayButtonOutput: falsenavPosition: "top"controlsContainer Options reference
The attributes the handler reads from the wrapper, and the tiny-slider options the configs set.
Attributes (on the wrapper)
| Attribute | From | What it does |
|---|---|---|
data-slider-items | 0px, default 2 | Slides visible at once. Becomes tiny-slider's items |
data-slider-items-tablets | 580px | Slides visible from this width up |
data-slider-items-tabletm | 810px | Slides visible from this width up |
data-slider-items-tabletl | 1024px | Slides visible from this width up |
data-slider-items-laptop | 1300px | Slides visible from this width up |
data-slider-items-desktop | 1570px | Slides visible from this width up |
data-slider-items-wide | 1700px | Slides 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
| Option | Values | What it does |
|---|---|---|
items | number, from data-slider-items | Slides visible at once. If there are as many slides or fewer, the slider is not initialised |
responsive | { 1024: { items: 4 }, … }, from the breakpoint attributes | Options per min-width |
nav | true (common), false (custom controls) | The dots, one per page of slides |
navPosition | "bottom" (common), "top" (autoplay) | Where the dots go, relative to the slides |
controls | false (common), true (both variations) | Prev and next buttons. With a focused button the arrow keys move the slider too |
controlsContainer | element.nextElementSibling (both variations) | An element with at least two children, used as the prev and next buttons instead of the injected ones |
autoplay | true (autoplay) | Advances on its own every autoplayTimeout ms, 5000 by default |
autoplayButtonOutput | false (autoplay) | Does not inject the start/stop button |
onInit | function (common) | Called by tiny-slider once built. The handler tweens the wrapper to full opacity with GSAP |
onSlideTransitionEnd | function (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.