v0.1 · pre-release

slick.jsless

A faithful port of slick without the jQuery — accessible by default, with a modern pointer-driven swipe gesture and a tiny gzip footprint.

  • 0dependencies
  • ~8kgzipped
  • ESM+ UMD + CJS
  • a11yby default
new Slickless("#hero-carousel", {
  slidesToShow: 1,
  centerMode: true,
  centerPadding: "32px",
  autoplay: true,
  autoplaySpeed: 3200,
  speed: 600,
  dots: true,
  arrows: false,
  infinite: true,
});

Quickstart

Three small steps and you have a carousel running. No build-time transform, no template compiler — just import the module and point it at any element.

  1. 01

    Install the package

    Pick your runtime — slickless has zero peer dependencies.

    bun add slickless
    pnpm add slickless
    npm i slickless
  2. 02

    Drop in your markup

    Any element with direct children works. Each child becomes a slide; their HTML is preserved as-is.

    <!-- index.html -->
    <div id="my-carousel">
      <div>Slide one — anything inside here is a slide.</div>
      <div>Slide two — an <img>, a <figure>, a whole card…</div>
      <div>Slide three.</div>
      <div>Slide four.</div>
    </div>
  3. 03

    Initialise from JavaScript

    Import the stylesheet once, then construct an instance. You can pass a CSS selector or an HTMLElement.

    // main.js
    import { Slickless } from "slickless";
    import "slickless/style.css";
    
    const carousel = new Slickless("#my-carousel", {
      slidesToShow: 3,
      infinite: true,
      dots: true,
    });
    
    // drive it programmatically
    carousel.next();
    carousel.on("afterChange", ({ currentSlide }) => {
      console.log("on slide", currentSlide);
    });

That's it. Scroll down for every option, method and event — or skip straight to the live examples.

Examples

Same option surface as slick. Different runtime — no jQuery, no monkey-patched prototypes, no globals.

Multiple slides, responsive

Three up on desktop, two on tablet, one on phone. Drag, tap, and arrow-key.

new Slickless("#multi-carousel", {
  slidesToShow: 3,
  slidesToScroll: 1,
  dots: true,
  infinite: true,
  responsive: [
    { breakpoint: 960, settings: { slidesToShow: 2 } },
    { breakpoint: 600, settings: { slidesToShow: 1 } },
  ],
});

Fade

Cross-fade between full-width slides. Autoplay, paused on focus and hover.

new Slickless("#fade-carousel", {
  fade: true,
  autoplay: true,
  autoplaySpeed: 3500,
  speed: 700,
  dots: true,
  arrows: false,
  infinite: true,
});

Vertical

Stack-up navigation, drag vertically.

new Slickless("#vertical-carousel", {
  vertical: true,
  slidesToShow: 3,
  slidesToScroll: 1,
  arrows: true,
  infinite: true,
  speed: 400,
});

Center mode

Focused slide centered; siblings peek with a soft scale-down. Drag the slider to feel centerPadding shrink the focused slide and expose more of the neighbours.

new Slickless("#center-carousel", {
  centerMode: true,
  centerPadding: "60px",
  slidesToShow: 3,
  focusOnSelect: true,
  infinite: true,
  speed: 400,
  arrows: true,
  responsive: [
    { breakpoint: 720, settings: { slidesToShow: 1, centerPadding: "30px" } },
  ],
});

Linked carousels (asNavFor)

A main stage paired with a thumbnail strip. Click thumbs to drive the main.

Solstice
Equinox
Eclipse
Auroral
Penumbra
// Initialise the nav first so the main can subscribe to it.
new Slickless("#nav-strip", {
  slidesToShow: 5,
  slidesToScroll: 1,
  arrows: false,
  focusOnSelect: true,
  infinite: false,
  draggable: false,
  speed: 300,
});

new Slickless("#main-stage", {
  slidesToShow: 1,
  arrows: true,
  fade: true,
  asNavFor: "#nav-strip",
  infinite: true,
  speed: 600,
});

Custom paging

Replace dots with anything — labels, thumbnails, fractions.

new Slickless("#custom-paging-carousel", {
  slidesToShow: 1,
  arrows: false,
  dots: true,
  speed: 500,
  infinite: true,
  customPaging: (i, total) => {
    const btn = document.createElement("button");
    btn.type = "button";
    btn.className = "frac";
    btn.setAttribute("aria-label", `Go to slide ${i + 1}`);
    btn.textContent = `${String(i + 1).padStart(2, "0")} / ${String(total).padStart(2, "0")}`;
    return btn;
  },
});

Adaptive height

The viewport resizes itself to whichever slide is in focus — handy when slides have very different aspect ratios. Flick the switch to feel the difference.

const carousel = new Slickless("#adaptive-carousel", {
  adaptiveHeight: true,
  dots: true,
  infinite: true,
  speed: 450,
});

// Toggle the option at runtime — setOptions rebuilds the carousel.
toggle.addEventListener("change", () =>
  carousel.setOptions({ adaptiveHeight: toggle.checked }));

Variable width

Each slide keeps its own intrinsic width — perfect for tag rails, chip lists or mixed-aspect cards. Toggle off to see every chip forced to the same width.

const carousel = new Slickless("#varwidth-carousel", {
  variableWidth: true,
  slidesToScroll: 1,
  infinite: true,
  speed: 350,
});

// Toggle the option at runtime — setOptions rebuilds the carousel.
toggle.addEventListener("change", () =>
  carousel.setOptions({ variableWidth: toggle.checked }));

Lazy loading images

Mark slow images with data-lazy. In ondemand mode the library swaps data-lazy for src only when a slide is about to come into view; in progressive mode it loads them all sequentially after init.

0 / 6 images loaded Navigate to load the next one.
<div id="lazy-carousel">
  <img data-lazy="…/mountain.jpg" alt="" />
  <img data-lazy="…/forest.jpg" alt="" />
  … more slides
</div>

const lazy = new Slickless("#lazy-carousel", {
  lazyLoad: "ondemand",  // or "progressive" / false
  dots: true,
  infinite: false,
});

// fires for every image as it resolves
lazy.on("lazyLoaded", ({ image, src }) =>
  console.log("loaded:", src));

Events

Subscribe with carousel.on(event, fn). Swipe, click an arrow or push the carousel past an edge — the log fills as events fire.

event log
  1. No events yet. Try the arrows or a swipe.
const carousel = new Slickless("#events-carousel", {
  dots: true,
  infinite: false,
  speed: 400,
});

// init — fires once after the carousel finishes building.
carousel.on("init", () => log("init"));

// beforeChange / afterChange — bracket every slide transition.
carousel.on("beforeChange", ({ currentSlide, nextSlide }) =>
  log(`beforeChange: ${currentSlide} → ${nextSlide}`));

carousel.on("afterChange", ({ currentSlide }) =>
  log(`afterChange: now on ${currentSlide}`));

// swipe — fires when a touch / pointer drag crosses the threshold.
carousel.on("swipe", ({ direction }) =>
  log(`swipe: ${direction}`));

// edge — fires when goTo bumps into the start or end of a finite carousel.
carousel.on("edge", ({ direction }) =>
  log(`edge hit: ${direction}`));

// You can also listen on the DOM:
// carousel.root.addEventListener("slickless:afterChange", e => ...);

Styling

slickless keeps styling out of the JS. Every visual concern — gaps, colours, arrows, dots — is a few lines of CSS in your own stylesheet.

Gap between slides

The library doesn't ship a default gap. Add horizontal padding to the slide wrapper — the JS-set width already uses box-sizing: border-box, so the padding eats into the slide and becomes the visible gap.

/* 12px gap between every slide (6px on each side). */
.my-carousel .slickless__slide {
  padding: 0 6px;
}

The slide count per viewport stays correct — the offset maths and slidesToShow are unaffected. Use a stronger selector to opt out for a specific carousel.

Theme via CSS variables

Arrows, dots and focus rings read from CSS custom properties on the .slickless root. Override them anywhere in your stylesheet.

.slickless {
  --slickless-arrow-size: 52px;
  --slickless-arrow-color: #fff;
  --slickless-arrow-bg: #0f172a;
  --slickless-arrow-bg-hover: #1e293b;
  --slickless-dot-color: rgba(255, 255, 255, 0.4);
  --slickless-dot-color-active: #fff;
  --slickless-focus-ring: 0 0 0 3px #818cf8;
}

Move the arrows outside the viewport

The library positions arrows at left: 12px / right: 12px. Pull them into a padded area on the carousel root if you want them out of the slide.

/* Give the carousel room on the sides, then pin arrows to the new edges. */
.my-carousel {
  padding: 0 60px;
}
.my-carousel .slickless__arrow--prev { left: 4px; }
.my-carousel .slickless__arrow--next { right: 4px; }

Avoid the pre-init layout jump

Before JavaScript runs, your children render as a tall vertical stack. After init, slickless rebuilds them into a flex row at a known height — that's a visible jolt unless the container reserves space ahead of time. Pick one of these strategies.

/* 1. Most reliable — give the root the height you actually want. */
#my-carousel { height: 320px; }

/* 2. Built-in helper — opt in by adding `class="slickless"` in your
 *    HTML. The library then hides everything but the first slide
 *    until init flips the `--initialized` class. */
<div id="my-carousel" class="slickless"></div>

/* 3. Or hide the whole thing while it boots. */
#my-carousel { visibility: hidden; }
#my-carousel.slickless--initialized { visibility: visible; }

Strategy 1 is the most predictable and pairs well with the other two. Strategy 2 is free if you can put the class in the markup; the helper rule lives in the library's stylesheet and only fires when both .slickless and not .slickless--initialized are present.

Per-slide state hooks

Slides receive class modifiers as the carousel updates. Target them to fade, scale or highlight the active range.

ClassWhen applied
.slickless__slide--activeSlide is within the visible window.
.slickless__slide--currentThe focused slide (the “leading” one).
.slickless__slide--clonedClone used for infinite looping.
.slickless__slide--centerCenter-mode focused slide.
.slickless__arrow--disabledEdge in non-infinite mode.

Options

All options are optional. Defaults are chosen to feel sensible out of the box.

OptionTypeDefaultNotes
arrowsbooleantrueShow prev / next arrows.
dotsbooleanfalseRender dot pagination.
infinitebooleantrueLoop slides via head/tail clones.
autoplaybooleanfalseAuto-advance slides.
autoplaySpeednumber3000Interval between auto-advances, ms.
autoplayDirection"forward" | "backward""forward"Direction of autoplay.
speednumber400Transition duration, ms.
cssEasestring"cubic-bezier(0.22, 1, 0.36, 1)"Any valid CSS easing string.
slidesToShownumber1How many slides visible per page.
slidesToScrollnumber1How many slides advance per move.
fadebooleanfalseUse cross-fade instead of slide.
verticalbooleanfalseStack slides vertically and slide on Y axis.
centerModebooleanfalseCentre the focused slide; peek siblings.
centerPaddingstring"50px"Padding around focused slide in center mode.
variableWidthbooleanfalseUse each slide's natural width.
initialSlidenumber0Starting slide index.
rtlbooleanfalseRight-to-left direction.
draggablebooleantrueEnable pointer / touch drag.
swipeThresholdnumber24Pixels needed to trigger a swipe.
pauseOnHoverbooleantruePause autoplay while hovered.
pauseOnFocusbooleantruePause autoplay while focus is inside.
accessibilitybooleantrueArrow keys, Home, End navigation.
adaptiveHeightbooleanfalseResize the viewport to current slide.
lazyLoad"ondemand" | "progressive" | falsefalseLazy load img[data-lazy].
respectReducedMotionbooleantrueSkip animations when the user prefers reduced motion.
focusOnSelectbooleanfalseClicking a slide navigates to it.
asNavForstring | HTMLElement | nullnullLink to another initialised carousel for two-way sync.
prevArrowstring | HTMLElement | nullnullCustom previous-arrow HTML or element.
nextArrowstring | HTMLElement | nullnullCustom next-arrow HTML or element.
customPaging(i, total) => HTMLElement | stringnullRender each dot button yourself.
responsiveResponsiveBreakpoint[]nullPer-breakpoint option overrides; "unslick" destroys.
ariaRoleDescriptionstring"carousel"Value for the root's aria-roledescription.

Public API

MethodReturnsNotes
next()voidAdvance by slidesToScroll.
prev()voidRetreat by slidesToScroll.
goTo(i, immediate?)voidJump to slide; immediate skips animation.
play() / pause()voidStart / stop autoplay.
setOptions(opts, refresh?)voidUpdate options; defaults to a full rebuild.
addSlide(el, i?)voidInsert a slide. Defaults to end.
removeSlide(i)voidRemove a slide by its real index.
getCurrentSlide()numberIndex of the currently focused slide.
getSlideCount()numberNumber of real slides (excluding clones).
getSlides()HTMLElement[]Real slide elements only.
on(event, fn)() => voidSubscribe; returns unsubscribe.
off(event, fn)voidRemove a listener.
destroy()voidRestore the original DOM and remove listeners.
reInit()voidRebuild the carousel with current options.

Events

EventDetailWhen
init{ slickless }After first build.
beforeChange{ currentSlide, nextSlide }Before slide transition starts.
afterChange{ currentSlide }After slide transition finishes.
swipe{ direction }User completed a swipe gesture.
edge{ direction }Reached start or end (non-infinite).
breakpoint{ breakpoint }Responsive breakpoint switched.
lazyLoaded{ image, src }A lazy image finished loading.
lazyLoadError{ image, src }A lazy image failed to load.
destroy{ slickless }Carousel was destroyed.
reInit{ slickless }Carousel was rebuilt.