Flickity Slider Setup (Watch CSS)

A responsive Flickity slider that uses Flickity's watchCSS option to enable or disable itself based on CSS, with custom prev/next buttons, dot pagination, slide count attributes, and multi-instance support.

sliderflickityresponsivewatch-csscss-variablespaginationdotsmulti-instance

Setup — External Scripts

Flickity 2.3.0 CSS
html
<link rel="stylesheet" href="https://unpkg.com/flickity@2.3.0/dist/flickity.css"/>
Flickity 2.3.0 JS
html
<script src="https://unpkg.com/flickity@2.3.0/dist/flickity.pkgd.min.js"></script>

Code

index.html
html
<div class="flickity-slider-group" data-flickity-status="not-active" data-flickity-type="cards" data-flickity-count>
  <div data-flickity-list="" class="flickity-list">
    <div data-flickity-item="" class="flickity-item">
      <div class="demo-card">
        <div class="demo-card__image">
          <div class="before__125"></div>
          <h2 class="demo-card__emoji">🎾</h2>
        </div>
        <h2 class="demo-card__h2">Tennis</h2>
      </div>
    </div>
    <div data-flickity-item="" class="flickity-item">
      <div class="demo-card">
        <div class="demo-card__image">
          <div class="before__125"></div>
          <h2 class="demo-card__emoji">⚾</h2>
        </div>
        <h2 class="demo-card__h2">Baseball</h2>
      </div>
    </div>
    <div data-flickity-item="" class="flickity-item">
      <div class="demo-card">
        <div class="demo-card__image">
          <div class="before__125"></div>
          <h2 class="demo-card__emoji">🏀</h2>
        </div>
        <h2 class="demo-card__h2">Basketball</h2>
      </div>
    </div>
    <div data-flickity-item="" class="flickity-item">
      <div class="demo-card">
        <div class="demo-card__image">
          <div class="before__125"></div>
          <h2 class="demo-card__emoji">⚽</h2>
        </div>
        <h2 class="demo-card__h2">Soccer</h2>
      </div>
    </div>
    <div data-flickity-item="" class="flickity-item">
      <div class="demo-card">
        <div class="demo-card__image">
          <div class="before__125"></div>
          <h2 class="demo-card__emoji">🏈</h2>
        </div>
        <h2 class="demo-card__h2">Football</h2>
      </div>
    </div>
    <div data-flickity-item="" class="flickity-item">
      <div class="demo-card">
        <div class="demo-card__image">
          <div class="before__125"></div>
          <h2 class="demo-card__emoji">🏐</h2>
        </div>
        <h2 class="demo-card__h2">Volleyball</h2>
      </div>
    </div>
    <div data-flickity-item="" class="flickity-item">
      <div class="demo-card">
        <div class="demo-card__image">
          <div class="before__125"></div>
          <h2 class="demo-card__emoji">🎱</h2>
        </div>
        <h2 class="demo-card__h2">Pool</h2>
      </div>
    </div>
  </div>
  <div data-flickity-controls="" class="flickity-controls">
    <div class="flickity-arrows">
      <div data-flickity-control="prev" class="flickity-arrow is--flipped">
        <svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 24 24" fill="none"><path d="M14 19L21 12L14 5" stroke="currentColor" stroke-miterlimit="10" stroke-width="2"></path><path d="M21 12H2" stroke="currentColor" stroke-miterlimit="10" stroke-width="2"></path></svg>
      </div>
      <div data-flickity-control="next" class="flickity-arrow">
        <svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 24 24" fill="none"><path d="M14 19L21 12L14 5" stroke="currentColor" stroke-miterlimit="10" stroke-width="2"></path><path d="M21 12H2" stroke="currentColor" stroke-miterlimit="10" stroke-width="2"></path></svg>
      </div>
    </div>
    <div class="flickity-dots">
      <div class="flickity-dots-list">
        <div data-flickity-dot="active" class="flickity-dot"></div>
        <div data-flickity-dot="" class="flickity-dot"></div>
        <div data-flickity-dot="" class="flickity-dot"></div>
        <div data-flickity-dot="" class="flickity-dot"></div>
        <div data-flickity-dot="" class="flickity-dot"></div>
        <div data-flickity-dot="" class="flickity-dot"></div>
        <div data-flickity-dot="" class="flickity-dot"></div>
      </div>
    </div>
  </div>
</div>
styles.css
css
.flickity-slider-group {
  width: 100%;
  position: relative;
}

.flickity-viewport {
  overflow: visible;
  width: 100%;
}

.flickity-list {
  width: 100%;
  display: flex;
}

.flickity-item {
  width: calc((99.99% / var(--flick-col)) - (var(--flick-gap) * ((var(--flick-col) - 1) / var(--flick-col))));
  margin-right: var(--flick-gap);
  flex-shrink: 0;
}

.flickity-controls {
  pointer-events: none;
  flex-flow: column;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  display: flex;
  position: absolute;
  top: 0;
  left: 0;
}

.flickity-arrows {
  pointer-events: none;
  justify-content: space-between;
  align-items: center;
  width: calc(100% + 3em);
  display: flex;
  position: relative;
}

.flickity-arrow {
  pointer-events: auto;
  color: #efeeec;
  cursor: pointer;
  background-color: #131313;
  border-radius: 50%;
  justify-content: center;
  align-items: center;
  width: 3em;
  height: 3em;
  padding-left: .75em;
  padding-right: .75em;
  display: flex;
}

.flickity-arrow.is--flipped {
  transform: scaleX(-1);
}

[data-flickity-control][disabled] {
  visibility: hidden;
  opacity: 0;
  pointer-events: none;
}

.flickity-dots {
  width: 100%;
  padding-top: 4em;
  position: absolute;
  top: 100%;
  left: 0;
}

.flickity-dots-list {
  grid-column-gap: .75em;
  grid-row-gap: .75em;
  justify-content: center;
  align-items: center;
  display: flex;
}

.flickity-dot {
  pointer-events: auto;
  background-color: #d0cfcd;
  border-radius: 50%;
  width: .75em;
  height: .75em;
  cursor: pointer;
}

[data-flickity-dot="active"] {
  background-color: #131313;
}

/* Turn Flickity on */
[data-flickity-status="active"] [data-flickity-list]::after {
  content: "flickity";
  display: none;
}

[data-flickity-status="active"] [data-flickity-list] {
  display: block;
}

/* ------------ Flickity Slider - Cards ------------ */

/* Desktop */
@media screen and (min-width: 992px) {
  [data-flickity-type="cards"] {
    --flick-col: 3;
    --flick-gap: 2em;
  }
  /* Turn Flickity OFF & Hide Controls */
  [data-flickity-type="cards"]:is([data-flickity-count="1"], [data-flickity-count="2"], [data-flickity-count="3"]) [data-flickity-list]::after { content: ""; display: block; }
  [data-flickity-type="cards"]:is([data-flickity-count="1"], [data-flickity-count="2"], [data-flickity-count="3"]) [data-flickity-list] { display: flex; }
  [data-flickity-type="cards"]:is([data-flickity-count="1"], [data-flickity-count="2"], [data-flickity-count="3"]) [data-flickity-controls] { display: none; }
  [data-flickity-type="cards"] [data-flickity-dot]:nth-last-child(-n+2) { display: none; } /* Hide last two dots */
}

/* Tablet */
@media (min-width: 768px) and (max-width: 991px) {
  [data-flickity-type="cards"] {
    --flick-col: 2.5;
    --flick-gap: 1.5em;
  }
  [data-flickity-type="cards"]:is([data-flickity-count="1"], [data-flickity-count="2"]) [data-flickity-list]::after { content: ""; display: block; }
  [data-flickity-type="cards"]:is([data-flickity-count="1"], [data-flickity-count="2"]) [data-flickity-list] { display: flex; }
  [data-flickity-type="cards"]:is([data-flickity-count="1"], [data-flickity-count="2"]) [data-flickity-controls] { display: none; }
  [data-flickity-type="cards"] [data-flickity-dot]:nth-last-child(1) { display: none; } /* Hide last dot */
}

/* Mobile */
@media screen and (max-width: 767px) {
  [data-flickity-type="cards"] {
    --flick-col: 1.5;
    --flick-gap: 1em;
  }
  [data-flickity-type="cards"]:is([data-flickity-count="1"]) [data-flickity-list]::after { content: ""; display: block; }
  [data-flickity-type="cards"]:is([data-flickity-count="1"]) [data-flickity-list] { display: flex; }
  [data-flickity-type="cards"]:is([data-flickity-count="1"]) [data-flickity-controls] { display: none; }
}

/* Demo Card */

.demo-card {
  grid-column-gap: 1.25em;
  grid-row-gap: 1.25em;
  background-color: #efeeec;
  border-radius: 1.5em;
  flex-flow: column;
  width: 100%;
  padding: 1em 1em 1.5em;
  display: flex;
  position: relative;
}

.before__125 {
  pointer-events: none;
  padding-top: 100%;
}

.demo-card__image {
  background-color: #e2e1df;
  border-radius: .5em;
  justify-content: center;
  align-items: center;
  width: 100%;
  display: flex;
  position: relative;
}

.demo-card__h2 {
  margin-top: 0;
  margin-bottom: 0;
  margin-left: .5em;
  font-size: 2em;
  font-weight: 500;
  line-height: 1;
}

.demo-card__emoji {
  margin-top: 0;
  margin-bottom: 0;
  font-size: 5em;
  font-weight: 500;
  line-height: 1;
  position: absolute;
}
script.js
javascript
function initFlickitySlider() {
  // Select all slider groups with the specified data attribute
  const sliderCards = document.querySelectorAll('[data-flickity-type="cards"]');

  sliderCards.forEach((slider, index) => {
    // Give each slider a unique ID
    const sliderIndexID = 'flickity-type-cards-id-' + index;
    slider.id = sliderIndexID;

    // Count slides
    let slidesCount = slider.querySelectorAll('[data-flickity-item]').length;
    slider.setAttribute('data-flickity-count', slidesCount);

    // Set Active status
    slider.setAttribute('data-flickity-status', 'active');

    // Select the element containing the slide list
    const sliderEl = document.querySelector('#' + sliderIndexID + ' [data-flickity-list]');
    if (!sliderEl) return;

    // Initialize Flickity on the slider element
    const flickitySlider = new Flickity(sliderEl, {
      watchCSS: true,
      contain: true,
      wrapAround: false,
      dragThreshold: 10,
      prevNextButtons: false,
      pageDots: false,
      cellAlign: 'left',
      selectedAttraction: 0.015,
      friction: 0.25,
      percentPosition: true,
      freeScroll: false,
      on: {
        dragStart: () => {
          // Disable pointer events during drag
          sliderEl.style.pointerEvents = "none";
        },
        dragEnd: () => {
          // Re-enable pointer events after drag
          sliderEl.style.pointerEvents = "auto";
        },
        change: function () {
          updateArrows();
          updateDots();
        }
      }
    });

    // Get Flickity instance data
    const flickity = Flickity.data(sliderEl);

    // Set up previous click functionality
    const prevButton = slider.querySelector('[data-flickity-control="prev"]');
    if (prevButton) {
      prevButton.setAttribute('disabled', '');
      prevButton.addEventListener('click', function () {
        flickity.previous();
      });
    }

    // Set up next click functionality
    const nextButton = slider.querySelector('[data-flickity-control="next"]');
    if (nextButton) {
      nextButton.addEventListener('click', function () {
        flickity.next();
      });
    }

    // Update arrows using CSS var(--flick-col) count
    function updateArrows() {
      const inviewColumns = parseInt(window.getComputedStyle(sliderEl).getPropertyValue('--flick-col'), 10);
      if (!flickity.cells[flickity.selectedIndex - 1]) {
        if (prevButton) prevButton.setAttribute('disabled', 'disabled');
        if (nextButton) nextButton.removeAttribute('disabled');
      } else if (!flickity.cells[flickity.selectedIndex + inviewColumns]) {
        if (nextButton) nextButton.setAttribute('disabled', 'disabled');
        if (prevButton) prevButton.removeAttribute('disabled');
      } else {
        if (prevButton) prevButton.removeAttribute('disabled');
        if (nextButton) nextButton.removeAttribute('disabled');
      }
    }

    // Set up dots click functionality
    const dots = slider.querySelectorAll('[data-flickity-dot]');
    if (dots.length) {
      dots.forEach((dot, index) => {
        dot.addEventListener('click', function () {
          const inviewColumns = parseInt(window.getComputedStyle(sliderEl).getPropertyValue('--flick-col'), 10);
          const maxIndex = flickity.cells.length - inviewColumns;
          let targetIndex = index;
          if (targetIndex > maxIndex) targetIndex = maxIndex;
          flickity.select(targetIndex);
        });
      });
    }

    // Update dots using CSS var(--flick-col) count
    function updateDots() {
      const inviewColumns = parseInt(window.getComputedStyle(sliderEl).getPropertyValue('--flick-col'), 10);
      const maxIndex = flickity.cells.length - inviewColumns;
      const activeIndex = flickity.selectedIndex < maxIndex ? flickity.selectedIndex : maxIndex;
      const dots = slider.querySelectorAll('[data-flickity-dot]');
      dots.forEach((dot, index) => {
        dot.setAttribute('data-flickity-dot', index === activeIndex ? 'active' : '');
      });
    }
  });
}

// Initialize Flickity Slider
document.addEventListener('DOMContentLoaded', function() {
  initFlickitySlider();
});

Attributes

NameTypeDefaultDescription
data-flickity-typestring"cards"Add to the outermost slider container to identify the slider type. The script selects all elements with data-flickity-type="cards" and initialises each independently. Use a different value (e.g. "gallery") to support multiple slider types on the same page.
data-flickity-status"active" | "not-active""not-active"Set to "not-active" in HTML. The script sets it to "active" on init. The CSS uses this to inject content: "flickity" into the ::after pseudo-element, which is how Flickity's watchCSS option detects whether to enable itself.
data-flickity-countnumberautoSet automatically by the script to the number of [data-flickity-item] elements. CSS uses this value in :is() selectors to hide controls and disable Flickity when all slides fit in the visible columns.
data-flickity-liststring""Add to the element Flickity is initialised on. When watchCSS enables Flickity, this element switches from display: flex to display: block so Flickity can take over layout.
data-flickity-itemstring""Add to each slide element. The script counts these to set data-flickity-count on the wrapper.
data-flickity-controlsstring""Add to the container holding arrows and dots. Hidden via CSS using display: none when the slide count is ≤ the visible column count for the current breakpoint.
data-flickity-control"prev" | "next"""Add to each navigation arrow element. The script binds click listeners and applies the disabled attribute when the slider is at the first or last reachable position, accounting for the current column count.
data-flickity-dot"active" | """"Add to each dot element. The script updates this attribute on every slide change to mark the active dot. Set the first dot to data-flickity-dot="active" in HTML for the initial state.

Notes

  • Flickity's watchCSS option checks for content: 'flickity' on the [data-flickity-list]::after pseudo-element. The CSS injects this string when data-flickity-status="active" is set, which the script does on init.
  • Slide width is calculated with a CSS calc() formula using --flick-col and --flick-gap. Changing these variables at breakpoints automatically resizes slides — no JS changes needed.
  • The script reads --flick-col via getComputedStyle on every arrow/dot update call, so responsive changes between breakpoints are always reflected correctly.
  • Dots are manually placed in HTML (one per slide) rather than generated by Flickity. The script handles dot activation and click navigation; the last N dots are hidden via CSS nth-last-child selectors to account for partially visible slides.
  • Multiple slider instances on the same page are fully supported — each is assigned a unique ID (flickity-type-cards-id-0, etc.) and all event listeners and state are scoped to that instance.
  • Pointer events on the slider list are disabled during drag and re-enabled on dragEnd to prevent accidental link or button activations while swiping.
  • The controls container uses pointer-events: none at the wrapper level, with individual arrow elements re-enabling pointer-events: auto — this ensures the arrows overlay the slider without blocking drag interaction on the slides behind them.

Guide

Slider Group, List & Items

Add data-flickity-type="cards" to the wrapper, data-flickity-list to the direct slide container, and data-flickity-item to each slide. The script assigns a unique ID, counts items, and sets data-flickity-status="active" to trigger Flickity via watchCSS.

Responsive columns & gap

Set --flick-col and --flick-gap on the slider container in CSS at each breakpoint. Slide widths are calculated automatically from these variables using calc().

@media screen and (min-width: 992px) {
  [data-flickity-type="cards"] {
    --flick-col: 3;
    --flick-gap: 2em;
  }
}

Auto-disable when all slides fit

Use :is() selectors on data-flickity-count to inject an empty ::after content string (overriding 'flickity') and switch the list back to display: flex when the slide count is ≤ the column count. Also hide controls.

[data-flickity-type="cards"]:is([data-flickity-count="1"], [data-flickity-count="2"], [data-flickity-count="3"]) [data-flickity-list]::after {
  content: "";
  display: block;
}
[data-flickity-type="cards"]:is([data-flickity-count="1"], [data-flickity-count="2"], [data-flickity-count="3"]) [data-flickity-list] {
  display: flex;
}
[data-flickity-type="cards"]:is([data-flickity-count="1"], [data-flickity-count="2"], [data-flickity-count="3"]) [data-flickity-controls] {
  display: none;
}

Prev/Next arrows (optional)

Add data-flickity-control="prev" and data-flickity-control="next" to any clickable elements inside the wrapper. The script binds click handlers and toggles the disabled attribute based on position and current column count. Style [data-flickity-control][disabled] to hide or fade disabled arrows.

Dots pagination (optional)

Add one [data-flickity-dot] element per slide inside a .flickity-dots-list container. Set the first dot to data-flickity-dot="active" in HTML. The script updates the active attribute on every slide change. Use nth-last-child in CSS to hide surplus dots that would point past the last reachable position.

/* Hide last two dots on desktop (3 columns visible) */
[data-flickity-type="cards"] [data-flickity-dot]:nth-last-child(-n+2) {
  display: none;
}

Multiple slider types

To add a second slider type (e.g. a gallery), query a different data-flickity-type value in JS, define its own --flick-col/--flick-gap breakpoints in CSS, and set its own disable-condition :is() selectors. Each type is fully independent.