Replacing a jQuery slider: controls, motion and lifecycle

How-to Last tested 24 October 2026

Replace a slider by keeping one selected item, mapping every control to that state, and giving its listeners and timer a clear lifetime. Start with manual navigation; add rotation only with an explicit start and stop, and restore ordinary page content when the component is removed.

Give every control the same state

The example has three items, Previous and Next buttons, three numbered choices and an optional rotation button. Its first mount selects the first item and wraps at either end. All three items remain ordinary document content before enhancement and after destruction.

Previous subtracts one from the selected index, Next adds one, and a numbered choice supplies an exact index. Normalize the result into the three-item range before rendering. One route into the renderer keeps the controls from developing different rules for the first and last items.

The selected index names the visible item. The numbered control’s current marker makes the same choice visible beside the content. Selecting that item again leaves both at the same destination.

The numbered controls use aria-current to mark the selected choice. Their nodes stay outside the changing content, so rendering an item does not replace the control the reader is using. The selection function changes visibility without requesting a focus move.

Make rotation an explicit choice

The W3C carousel pattern calls for a rotation control, stopping when focus or the pointer enters, and controls that do not move focus when activated.1 Treat those as parts of the interaction contract, alongside the initial item and end behavior.

This component mounts without rotation and changes items immediately. An explicit start request creates a three-second timer. Focus entry, pointer entry, Escape, destruction and the reduced-motion branch all call the same stop function. Stopping never schedules a restart: the reader must start again.

The prefers-reduced-motion query reports the device preference described by MDN.2 This component refuses a rotation request when that query matches and stops an existing timer when the preference changes to reduce. There is no simulation control in this example.

Keep cleanup with the instance

One AbortController owns the mounted instance’s listeners. Passing its signal to addEventListener lets abort remove those listeners together.3 The timer is a different resource and must be cleared separately. Removing event listeners does not cancel a scheduled interval.

The instance guard makes a repeated Mount return early. Destroy stops rotation, aborts the component listeners, restores the static items and disables the component controls. Mount and Destroy remain lifecycle controls. Their listeners stay outside the mounted instance so that the component can be created again after its listeners are released.

Keep cleanup limited to resources the component owns. An application that relies on callbacks needs those consumers mapped separately; deleting a dependency does not remove their requirements. A page-level controller can destroy this instance without treating every event listener on the page as part of it.

The complete example

Save this original, MIT-licensed HTML as a local file. It includes the content, controls, timer, repeated-mount guard and cleanup path. The SPDX comment identifies its licence. It needs no external dependency.

<!doctype html>
<!-- SPDX-License-Identifier: MIT -->
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Slider controls and cleanup</title>
<style>
  [hidden] { display: none !important; }
  :focus-visible { outline: 3px solid #a43b00; outline-offset: 2px; }
  [aria-current="true"] { font-weight: bold; }
</style>
<button id="destroy">Destroy slider</button>
<button id="mount">Mount slider</button>
<section id="slider" aria-label="Project notes" aria-roledescription="carousel">
  <div id="controls" hidden>
    <button id="rotation">Start rotation</button>
    <button id="previous" data-step="-1">Previous</button>
    <button id="next" data-step="1">Next</button>
    <button id="pick-1" data-index="0">1</button>
    <button id="pick-2" data-index="1">2</button>
    <button id="pick-3" data-index="2">3</button>
  </div>
  <article data-slide>
    <h2>One: define the contract</h2>
    <p>List the controls and callbacks the page needs.</p>
    <a href="#after">Continue after the slider</a>
  </article>
  <article data-slide>
    <h2>Two: own the lifetime</h2>
    <p>One instance owns its listeners and timer.</p>
    <a href="#after">Continue after the slider</a>
  </article>
  <article data-slide>
    <h2>Three: keep an exit</h2>
    <p>Destroy restores readable content.</p>
    <a href="#after">Continue after the slider</a>
  </article>
</section>
<button id="after">After the slider</button>
<script>
  const root = document.querySelector('#slider');
  const slides = [...root.querySelectorAll('[data-slide]')];
  const controls = document.querySelector('#controls');
  const rotation = document.querySelector('#rotation');
  const preference = matchMedia('(prefers-reduced-motion: reduce)');
  let controller = null, timer = null, index = 0;
  function stop() {
    clearInterval(timer);
    timer = null;
    rotation.textContent = 'Start rotation';
  }
  function show(next) {
    index = (next + slides.length) % slides.length;
    slides.forEach((slide, i) => { slide.hidden = i !== index; });
    root.querySelectorAll('[data-index]').forEach(button => {
      button.setAttribute('aria-current', String(Number(button.dataset.index) === index));
    });
  }
  function mount() {
    if (controller) return;
    controller = new AbortController();
    const signal = controller.signal;
    controls.hidden = false;
    controls.querySelectorAll('button').forEach(button => { button.disabled = false; });
    root.addEventListener('click', event => {
      const button = event.target.closest('button');
      if (!button) return;
      if (button.dataset.step || button.dataset.index) {
        stop();
        show(button.dataset.step ? index + Number(button.dataset.step) : Number(button.dataset.index));
      }
      if (button === rotation) {
        if (timer || preference.matches) stop();
        else {
          timer = setInterval(() => show(index + 1), 3000);
          rotation.textContent = 'Stop rotation';
        }
      }
    }, { signal });
    root.addEventListener('focusin', stop, { signal });
    root.addEventListener('pointerenter', stop, { signal });
    document.addEventListener('keydown', event => {
      if (event.key === 'Escape') stop();
    }, { signal });
    preference.addEventListener('change', () => {
      if (preference.matches) stop();
    }, { signal });
    show(index);
  }
  function destroy() {
    if (!controller) return;
    stop();
    controller.abort();
    controller = null;
    slides.forEach(slide => { slide.hidden = false; });
    controls.querySelectorAll('button').forEach(button => { button.disabled = true; });
  }
  document.querySelector('#destroy').addEventListener('click', destroy);
  document.querySelector('#mount').addEventListener('click', mount);
  mount();
</script>
</html>

Walk through the awkward cases

The demo retains the instrumented version. Use the trimmed HTML above for these component cases, and follow the state changes rather than just the picture:

Follow the lifecycle in that order before connecting real content or application callbacks. Compare the visible item with the current marker after each action. Check the control’s focus separately from the selected content, and allow enough time to distinguish a manual change from a timer tick.

Replacing an existing integration

Map the application’s controls, callbacks, initial item and cleanup requirements before replacing a jQuery slider. Retain a rollback point while changing the integration. This route contains original code and prose; it does not reproduce a former plugin or promise compatibility with its API.

Not covered on this page

Sources

  1. W3C, Carousel pattern, ARIA Authoring Practices Guide. Living documentation, opened 24 October 2026. ↩

  2. MDN, prefers-reduced-motion. Living documentation, opened 24 October 2026. ↩

  3. MDN, EventTarget.addEventListener(). Living documentation, opened 24 October 2026. ↩

Code on this page is original and MIT licensed.

How we test: methodology. Found a mistake? Corrections.