SnippetStable

DX Slider

A complete dependency-free JavaScript slider with slide, fade and scale effects, element animations, looping, swipe gestures, autoplay, keyboard controls and a public API.

Published
17 Jan 2019
Updated
7 Nov 2019
Version
v1.0.3
License
MIT

A closer look

Screenshots

The useful part

What it does

DX Slider is a reusable, responsive carousel built with plain JavaScript and CSS. It supports multiple instances and keeps navigation, pagination, playback, accessibility state and slide animations in sync.

Transitions and motion

  • effect: slide, fade or scale; change it at setup or later with setEffect()
  • duration and easing: control transition timing
  • animateElements, elementDuration and elementStagger: animate content inside the active slide
  • data-dx-animate: fade, fade-up, fade-down, fade-left, fade-right or zoom, with optional per-element delay and duration

Playback and navigation

  • autoplay, loop, startAt and stopOnInteraction
  • pauseOnHover and pauseOnFocus, plus automatic pause for dragging and hidden tabs
  • keyboard, navigation, pagination, draggable and swipeThreshold

Layout, loading and integration

  • autoHeight for slides with different content heights
  • lazyLoad for nearby images using data-dx-src and data-dx-srcset
  • onInit, onBeforeChange, onChange and onDestroy callbacks
  • Custom init, before-change, change, effect-change and destroy events
  • Public next, previous, goTo, play, pause, setEffect, refresh, getState and destroy methods

Inspect and adapt

The code

The demo files are shown below. Download the complete package for the slider source, styles and demo.

index.html
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>DX Slider</title>
    <link rel="stylesheet" href="dx-slider.css">
    <link rel="stylesheet" href="demo.css">
</head>
<body>
    <main class="demo-shell">
        <header class="demo-intro">
            <div>
                <p class="demo-kicker">No dependencies</p>
                <h1>DX Slider</h1>
                <p>Responsive, swipeable and configurable for real projects.</p>
            </div>
            <div class="demo-effects" aria-label="Transition effect">
                <span>Effect</span>
                <button type="button" data-demo-effect="slide" aria-pressed="true">Slide</button>
                <button type="button" data-demo-effect="fade" aria-pressed="false">Fade</button>
                <button type="button" data-demo-effect="scale" aria-pressed="false">Scale</button>
            </div>
        </header>

        <section class="dx-slider js-dx-slider" aria-label="Featured capabilities">
            <div class="dx-viewport">
                <div class="dx-track">
                    <article class="dx-slide dx-slide--mint" data-title="Responsive">
                        <div class="dx-slide__copy">
                            <span class="dx-slide__number" data-dx-animate="fade">01</span>
                            <p class="dx-slide__eyebrow" data-dx-animate="fade-up">Any screen</p>
                            <h2 data-dx-animate="fade-up">Built to fit.</h2>
                            <p data-dx-animate="fade-up">The track is measured again whenever the viewport changes.</p>
                        </div>
                        <div class="dx-art dx-art--frames" aria-hidden="true"><i></i><i></i><i></i></div>
                    </article>

                    <article class="dx-slide dx-slide--blue" data-title="Touch">
                        <div class="dx-slide__copy">
                            <span class="dx-slide__number" data-dx-animate="fade">02</span>
                            <p class="dx-slide__eyebrow" data-dx-animate="fade-right">Mouse + touch</p>
                            <h2 data-dx-animate="fade-right">Drag naturally.</h2>
                            <p data-dx-animate="fade-right">Horizontal intent is detected before the page scroll is interrupted.</p>
                        </div>
                        <div class="dx-art dx-art--swipe" aria-hidden="true"><i></i><b></b><span>←  →</span></div>
                    </article>

                    <article class="dx-slide dx-slide--coral" data-title="Controls">
                        <div class="dx-slide__copy">
                            <span class="dx-slide__number" data-dx-animate="fade">03</span>
                            <p class="dx-slide__eyebrow" data-dx-animate="fade-up">Keyboard ready</p>
                            <h2 data-dx-animate="zoom">Every way in.</h2>
                            <p data-dx-animate="fade-up">Use arrow keys, Home, End, pagination, buttons or a swipe.</p>
                        </div>
                        <div class="dx-art dx-art--keys" aria-hidden="true"><i>←</i><i>→</i><i>home</i><i>end</i></div>
                    </article>

                    <article class="dx-slide dx-slide--gold" data-title="Autoplay">
                        <div class="dx-slide__copy">
                            <span class="dx-slide__number" data-dx-animate="fade">04</span>
                            <p class="dx-slide__eyebrow" data-dx-animate="fade-left">Considerate autoplay</p>
                            <h2 data-dx-animate="fade-left">Moves, then waits.</h2>
                            <p data-dx-animate="fade-left">Playback pauses for hover, focus, dragging and hidden browser tabs.</p>
                        </div>
                        <div class="dx-art dx-art--clock" aria-hidden="true"><i></i><b></b><span></span></div>
                    </article>

                    <article class="dx-slide dx-slide--violet" data-title="API">
                        <div class="dx-slide__copy">
                            <span class="dx-slide__number" data-dx-animate="fade">05</span>
                            <p class="dx-slide__eyebrow" data-dx-animate="fade-up">Public API</p>
                            <h2 data-dx-animate="fade-up">Make it yours.</h2>
                            <p data-dx-animate="fade-up">Navigate, control playback, switch effects, inspect state or destroy the instance.</p>
                        </div>
                        <pre class="dx-art dx-art--code" aria-hidden="true"><code>slider.goTo(3);
slider.setEffect('fade');
slider.pause();</code></pre>
                    </article>
                </div>
            </div>

            <button class="dx-arrow dx-arrow--previous" type="button" data-dx-previous aria-label="Previous slide">←</button>
            <button class="dx-arrow dx-arrow--next" type="button" data-dx-next aria-label="Next slide">→</button>

            <footer class="dx-footer">
                <div class="dx-count" aria-hidden="true">
                    <strong data-dx-current>01</strong><span></span><b data-dx-total>05</b>
                </div>
                <div class="dx-dots" data-dx-dots></div>
                <button class="dx-play" type="button" data-dx-play aria-label="Pause autoplay"><i></i><span>Pause</span></button>
            </footer>
            <p class="dx-status" data-dx-status aria-live="polite" aria-atomic="true"></p>
        </section>

        <p class="demo-hint">Drag the slide, use the controls, or press the arrow keys.</p>

        <div class="demo-examples">
            <header>
                <p class="demo-kicker">More setups</p>
                <h2>Examples</h2>
                <p>The same script with different options. View the page source to copy any of them.</p>
            </header>

            <section class="demo-example">
                <header>
                    <h3>Fade, hands-free</h3>
                    <p>A crossfade that runs on its own: <code>effect: 'fade'</code> with <code>autoplay: 3500</code>. Hovering, focusing or hiding the tab pauses playback, and the play button stays in sync.</p>
                </header>
                <section class="dx-slider demo-compact" id="example-fade" aria-label="Fade with autoplay example">
                    <div class="dx-viewport">
                        <div class="dx-track">
                            <article class="dx-slide dx-slide--coral" data-title="Fade one">
                                <div class="dx-slide__copy">
                                    <p class="dx-slide__eyebrow">Fade</p>
                                    <h3>Nothing moves sideways.</h3>
                                    <p>Slides sit on top of each other and cross-dissolve.</p>
                                </div>
                            </article>
                            <article class="dx-slide dx-slide--blue" data-title="Fade two">
                                <div class="dx-slide__copy">
                                    <p class="dx-slide__eyebrow">Autoplay</p>
                                    <h3>Every 3.5 seconds.</h3>
                                    <p>The delay is the <code>autoplay</code> value in milliseconds.</p>
                                </div>
                            </article>
                            <article class="dx-slide dx-slide--violet" data-title="Fade three">
                                <div class="dx-slide__copy">
                                    <p class="dx-slide__eyebrow">Polite</p>
                                    <h3>It knows when to stop.</h3>
                                    <p>Hover this slide and the countdown quietly waits for you.</p>
                                </div>
                            </article>
                        </div>
                    </div>
                    <footer class="dx-footer">
                        <div class="dx-dots" data-dx-dots></div>
                        <button class="dx-play" type="button" data-dx-play aria-label="Pause autoplay"><i></i><span>Pause</span></button>
                    </footer>
                    <p class="dx-status" data-dx-status aria-live="polite" aria-atomic="true"></p>
                </section>
            </section>

            <section class="demo-example">
                <header>
                    <h3>Auto height</h3>
                    <p>With <code>autoHeight: true</code> the viewport animates to the height of the active slide, so short and long content can share one slider. <code>loop: false</code> gives the track hard ends and disables the arrows there.</p>
                </header>
                <section class="dx-slider demo-compact demo-autoheight" id="example-autoheight" aria-label="Auto height example">
                    <div class="dx-viewport">
                        <div class="dx-track">
                            <article class="dx-slide dx-slide--mint" data-title="Short">
                                <div class="dx-slide__copy">
                                    <p class="dx-slide__eyebrow">Short slide</p>
                                    <h3>One line.</h3>
                                    <p>This slide barely says anything.</p>
                                </div>
                            </article>
                            <article class="dx-slide dx-slide--gold" data-title="Medium">
                                <div class="dx-slide__copy">
                                    <p class="dx-slide__eyebrow">Medium slide</p>
                                    <h3>A little more.</h3>
                                    <p>This one has a couple of sentences, so it is naturally taller than the first. Watch the container ease to the new height instead of jumping.</p>
                                </div>
                            </article>
                            <article class="dx-slide dx-slide--blue" data-title="Long">
                                <div class="dx-slide__copy">
                                    <p class="dx-slide__eyebrow">Long slide</p>
                                    <h3>The tall one.</h3>
                                    <p>Auto height is made for content like this: a longer paragraph that would otherwise force every other slide to reserve the same space. The viewport measures the active slide and transitions its height using the same duration and easing as the slide animation, so the whole component feels consistent. Nothing below the slider jumps around unexpectedly — it simply eases out of the way.</p>
                                </div>
                            </article>
                        </div>
                    </div>
                    <button class="dx-arrow dx-arrow--previous" type="button" data-dx-previous aria-label="Previous slide">←</button>
                    <button class="dx-arrow dx-arrow--next" type="button" data-dx-next aria-label="Next slide">→</button>
                    <footer class="dx-footer">
                        <div class="dx-dots" data-dx-dots></div>
                    </footer>
                    <p class="dx-status" data-dx-status aria-live="polite" aria-atomic="true"></p>
                </section>
            </section>

            <section class="demo-example">
                <header>
                    <h3>Drag only, no chrome</h3>
                    <p>All controls removed: <code>navigation</code>, <code>pagination</code> and <code>keyboard</code> are off, leaving pure drag and swipe with <code>loop: false</code>. Useful inside card layouts where arrows would be noise.</p>
                </header>
                <section class="dx-slider demo-compact demo-bare" id="example-drag" aria-label="Drag only example">
                    <div class="dx-viewport">
                        <div class="dx-track">
                            <article class="dx-slide dx-slide--violet" data-title="Drag one">
                                <div class="dx-slide__copy">
                                    <p class="dx-slide__eyebrow">01 / 03</p>
                                    <h3>Grab this slide.</h3>
                                    <p>Drag with a mouse or swipe with a finger to move on.</p>
                                </div>
                            </article>
                            <article class="dx-slide dx-slide--coral" data-title="Drag two">
                                <div class="dx-slide__copy">
                                    <p class="dx-slide__eyebrow">02 / 03</p>
                                    <h3>Halfway there.</h3>
                                    <p>A short pull past <code>swipeThreshold</code> commits the change; anything less snaps back.</p>
                                </div>
                            </article>
                            <article class="dx-slide dx-slide--mint" data-title="Drag three">
                                <div class="dx-slide__copy">
                                    <p class="dx-slide__eyebrow">03 / 03</p>
                                    <h3>The end of the track.</h3>
                                    <p>With looping off, dragging further resists and settles back here.</p>
                                </div>
                            </article>
                        </div>
                    </div>
                    <p class="dx-status" data-dx-status aria-live="polite" aria-atomic="true"></p>
                </section>
            </section>

            <section class="demo-example">
                <header>
                    <h3>Remote control</h3>
                    <p>Any element on the page can drive a slider through its public API. These buttons live outside the component and call methods on the instance; the readout comes from <code>getState()</code>.</p>
                </header>
                <section class="dx-slider demo-compact demo-bare" id="example-remote" aria-label="Remote control example">
                    <div class="dx-viewport">
                        <div class="dx-track">
                            <article class="dx-slide dx-slide--gold" data-title="Remote one">
                                <div class="dx-slide__copy">
                                    <p class="dx-slide__eyebrow">External buttons</p>
                                    <h3>Steered from outside.</h3>
                                    <p>This slider has no built-in controls at all.</p>
                                </div>
                            </article>
                            <article class="dx-slide dx-slide--blue" data-title="Remote two">
                                <div class="dx-slide__copy">
                                    <p class="dx-slide__eyebrow">Programmatic</p>
                                    <h3>Slide two.</h3>
                                    <p>You arrived here through the API, not a swipe.</p>
                                </div>
                            </article>
                            <article class="dx-slide dx-slide--violet" data-title="Remote three">
                                <div class="dx-slide__copy">
                                    <p class="dx-slide__eyebrow">goTo(2)</p>
                                    <h3>Jumped straight in.</h3>
                                    <p>Indexes are zero-based, so this is slide number three.</p>
                                </div>
                            </article>
                        </div>
                    </div>
                    <p class="dx-status" data-dx-status aria-live="polite" aria-atomic="true"></p>
                </section>
                <div class="demo-remote" role="group" aria-label="External slider controls">
                    <button type="button" data-remote="previous">previous()</button>
                    <button type="button" data-remote="next">next()</button>
                    <button type="button" data-remote="goto">goTo(2)</button>
                    <button type="button" data-remote="play">play()</button>
                    <button type="button" data-remote="pause">pause()</button>
                    <span class="demo-remote__state" id="remote-readout" aria-live="polite"></span>
                </div>
            </section>

            <section class="demo-example">
                <header>
                    <h3>DOM events</h3>
                    <p>Every change dispatches bubbling DOM events on the root element, so other scripts can react without callbacks. This log listens for <code>dxsliderbeforechange</code> and <code>dxsliderchange</code>.</p>
                </header>
                <section class="dx-slider demo-compact" id="example-events" aria-label="DOM events example">
                    <div class="dx-viewport">
                        <div class="dx-track">
                            <article class="dx-slide dx-slide--blue" data-title="Events one">
                                <div class="dx-slide__copy">
                                    <p class="dx-slide__eyebrow">Listen in</p>
                                    <h3>Everything is announced.</h3>
                                    <p>Move this slider and watch the log below fill up.</p>
                                </div>
                            </article>
                            <article class="dx-slide dx-slide--mint" data-title="Events two">
                                <div class="dx-slide__copy">
                                    <p class="dx-slide__eyebrow">event.detail</p>
                                    <h3>With context attached.</h3>
                                    <p>Each event carries the previous index, the new index and the slide element.</p>
                                </div>
                            </article>
                            <article class="dx-slide dx-slide--coral" data-title="Events three">
                                <div class="dx-slide__copy">
                                    <p class="dx-slide__eyebrow">Analytics ready</p>
                                    <h3>Track it anywhere.</h3>
                                    <p>Wire the same events into analytics, lazy loading or your own UI.</p>
                                </div>
                            </article>
                        </div>
                    </div>
                    <button class="dx-arrow dx-arrow--previous" type="button" data-dx-previous aria-label="Previous slide">←</button>
                    <button class="dx-arrow dx-arrow--next" type="button" data-dx-next aria-label="Next slide">→</button>
                    <footer class="dx-footer">
                        <div class="dx-dots" data-dx-dots></div>
                    </footer>
                    <p class="dx-status" data-dx-status aria-live="polite" aria-atomic="true"></p>
                </section>
                <ol class="demo-log" id="event-log" aria-label="Slider event log">
                    <li>Waiting for the first event…</li>
                </ol>
            </section>
        </div>
    </main>

    <script src="dx-slider.js"></script>
    <script>
        var effectMatch = window.location.search.match(/[?&]effect=(slide|fade|scale)(?:&|$)/);
        var initialEffect = effectMatch ? effectMatch[1] : 'slide';
        var demoSlider = new DXSlider(document.querySelector('.js-dx-slider'), {
            effect: initialEffect,
            autoplay: 5000,
            duration: 600,
            easing: 'ease',
            loop: true,
            animateElements: true,
            elementStagger: 110
        });

        var effectButtons = document.querySelectorAll('[data-demo-effect]');
        var effectIndex;
        for (effectIndex = 0; effectIndex < effectButtons.length; effectIndex += 1) {
            effectButtons[effectIndex].setAttribute('aria-pressed', effectButtons[effectIndex].getAttribute('data-demo-effect') === initialEffect ? 'true' : 'false');
            effectButtons[effectIndex].addEventListener('click', function () {
                var buttonIndex;
                demoSlider.setEffect(this.getAttribute('data-demo-effect'));
                for (buttonIndex = 0; buttonIndex < effectButtons.length; buttonIndex += 1) {
                    effectButtons[buttonIndex].setAttribute('aria-pressed', effectButtons[buttonIndex] === this ? 'true' : 'false');
                }
            });
        }

        new DXSlider(document.getElementById('example-fade'), {
            effect: 'fade',
            autoplay: 3500,
            duration: 700
        });

        new DXSlider(document.getElementById('example-autoheight'), {
            autoHeight: true,
            loop: false
        });

        new DXSlider(document.getElementById('example-drag'), {
            navigation: false,
            pagination: false,
            keyboard: false,
            loop: false
        });

        var remoteSlider = new DXSlider(document.getElementById('example-remote'), {
            navigation: false,
            pagination: false,
            keyboard: false,
            onChange: function () {
                updateRemoteReadout(this);
            }
        });

        function updateRemoteReadout(slider) {
            var state = slider.getState();
            document.getElementById('remote-readout').textContent = 'getState(): slide ' + (state.index + 1) + ' of ' + state.total + (state.playing ? ', playing' : ', paused');
        }

        var remoteButtons = document.querySelectorAll('[data-remote]');
        var remoteIndex;
        for (remoteIndex = 0; remoteIndex < remoteButtons.length; remoteIndex += 1) {
            remoteButtons[remoteIndex].addEventListener('click', function () {
                var action = this.getAttribute('data-remote');
                if (action === 'goto') {
                    remoteSlider.goTo(2);
                } else {
                    remoteSlider[action]();
                }
                updateRemoteReadout(remoteSlider);
            });
        }
        updateRemoteReadout(remoteSlider);

        var eventRoot = document.getElementById('example-events');
        var eventLog = document.getElementById('event-log');
        new DXSlider(eventRoot, { autoplay: 4200 });

        function logSliderEvent(event) {
            var detail = event.detail || {};
            var row = document.createElement('li');
            var text = event.type;
            if (typeof detail.previousIndex === 'number') {
                text += '  { previousIndex: ' + detail.previousIndex + ', index: ' + detail.index + ' }';
            }
            row.textContent = text;
            eventLog.insertBefore(row, eventLog.firstChild);
            while (eventLog.children.length > 5) {
                eventLog.removeChild(eventLog.lastChild);
            }
        }

        eventRoot.addEventListener('dxsliderbeforechange', logSliderEvent);
        eventRoot.addEventListener('dxsliderchange', logSliderEvent);
    </script>
</body>
</html>

From zero to working

Install & use

  1. 01

    Copy dx-slider.css and dx-slider.js into your project and load both on the page.

  2. 02

    Use the HTML structure from the demo: one .dx-viewport containing a .dx-track and one or more .dx-slide elements.

  3. 03

    Add optional controls with the data-dx-previous, data-dx-next, data-dx-dots, data-dx-play, data-dx-current, data-dx-total and data-dx-status attributes.

  4. 04

    Add data-dx-animate to slide content that should animate when its slide becomes active. Use data-dx-delay and data-dx-duration for individual timing.

  5. 05

    Create an instance with new DXSlider(element, options). Set effect to slide, fade or scale, and set autoplay to a delay in milliseconds or 0 to disable it.

  6. 06

    Keep each slider instance if you need to call its public methods or destroy it later.

Try it first

Live demo

Know before using

Caveats & FAQ

  • The component displays one slide at a time; multi-column carousels need different sizing and movement calculations.
  • Slides are collected during setup. Destroy and create the instance again after adding or removing slides.
  • Loop mode clones the first and last slides. Avoid forms, video players or other stateful controls inside those boundary slides.

The contract

Options & API

Options

NameTypeDefaultDescription
effectstring'slide'Transition effect: 'slide', 'fade' or 'scale'.
durationnumber600Slide transition duration in milliseconds.
easingstring'ease'CSS easing function used for transitions.
autoplaynumber0Delay between automatic advances in milliseconds. 0 disables autoplay.
loopbooleantrueLoop seamlessly past the first and last slide using boundary clones.
startAtnumber0Zero-based index of the slide shown first.
keyboardbooleantrueMove between slides with the arrow keys while the slider has focus.
navigationbooleantrueWire up elements with data-dx-previous and data-dx-next as arrow controls.
paginationbooleantrueBuild dot pagination inside the data-dx-dots element.
draggablebooleantrueChange slides by dragging with a mouse, pen or touch.
swipeThresholdnumber50Distance in pixels a drag must travel before the slide changes.
pauseOnHoverbooleantruePause autoplay while the pointer is over the slider.
pauseOnFocusbooleantruePause autoplay while focus is inside the slider.
stopOnInteractionbooleanfalseStop autoplay for good after the visitor navigates manually, instead of resuming.
autoHeightbooleanfalseAnimate the viewport height to match the active slide.
lazyLoadbooleantrueLoad data-dx-src images only when their slide is active or adjacent.
animateElementsbooleantrueReplay data-dx-animate content animations when a slide becomes active.
elementDurationnumber600Duration of element animations in milliseconds.
elementStaggernumber100Delay in milliseconds between each animated element in a slide.

Methods

NameTypeDefaultDescription
next()Advance to the next slide.
previous()Go back to the previous slide.
goTo(index, interaction)Go to a zero-based slide index. Pass true as interaction to count it as a manual action for stopOnInteraction.
play()Start or resume autoplay.
pause()Pause autoplay until play() is called.
setEffect(effect)Switch to 'slide', 'fade' or 'scale' at runtime.
update(announce)Recalculate sizes and positions after a layout change.
getState()Return the current index, slide element and slide count.
destroy()Remove listeners, clones and generated markup, restoring the original element.

Events

NameTypeDefaultDescription
onInitcallbacknullRuns once the slider is ready. Receives the initial state.
onBeforeChangecallbacknullRuns before a transition starts. Receives the previous and target index.
onChangecallbacknullRuns after a transition completes. Receives the active index and slide element.
onDestroycallbacknullRuns after destroy() finishes.
dxsliderinitDOM eventFired on the root element when the slider is ready.
dxsliderbeforechangeDOM eventFired before a transition starts, with the indexes in event.detail.
dxsliderchangeDOM eventFired after a transition completes, with the state in event.detail.
dxslidereffectchangeDOM eventFired when the effect is switched at runtime.
dxsliderdestroyDOM eventFired on the root element after teardown.

Release notes

Changelog

  1. v1.0.3

    Refined inactive-slide focus handling and screen-reader announcements; fixed teardown so author attributes and inline styles are restored cleanly.

  2. v1.0.2

    Improved adjacent-slide lazy loading and corrected auto-height behaviour for fade, scale and resized sliders.

  3. v1.0.1

    Smoothed horizontal drag intent, tightened autoplay timer resets and fixed boundary-clone content animations during seamless looping.

  4. v1.0.0

    Initial release with slide, fade and scale effects; element animations; looping; drag and touch gestures; autoplay; keyboard controls; pagination; lazy loading; auto-height; accessibility state; callbacks; custom events; runtime controls and complete teardown.

Why it exists

Era & lineage

This started as a need for a simple slider that stays understandable when the feature list grows. The component keeps its structure explicit, small, exposes a small public API and can be dropped into a page without bringing along a framework or plugin.

Share this experimentUseful things travel further.
Link copied

Feedback

Comments

No comments yet. Found a bug or used it somewhere interesting? Say so below.

Leave a Reply

Your email address will not be published. Required fields are marked *

Copied to clipboard
Free intro call Choose a time that works.

Please book thoughtfully. Choose a time only if you have a real project or technical problem to discuss, or we are already in contact. For general questions, please use the contact form.