How to Replace swiper.js with Pure CSS Scroll‑Snap and Scroll‑Driven Animations

This guide shows how to drop the heavyweight swiper.js library by using modern CSS features such as scroll‑snap, scroll‑timeline, timeline‑scope, and animation‑iteration to build a lightweight, performant carousel with custom indicators, autoplay, and cross‑browser fallbacks.

Full-Stack Cultivation Path
Full-Stack Cultivation Path
Full-Stack Cultivation Path
How to Replace swiper.js with Pure CSS Scroll‑Snap and Scroll‑Driven Animations

1. CSS Scroll Snap

Define the scrolling container with scroll-snap-type and each slide with scroll-snap-align to obtain a horizontal carousel that snaps to each item.

<div class="swiper">
  <div class="swiper-item"><div class="card"></div></div>
  <div class="swiper-item"><div class="card"></div></div>
  <div class="swiper-item"><div class="card"></div></div>
</div>

Make the container a flexbox and enable overflow scrolling:

.swiper {
  display: flex;
  overflow: auto;
}
.swiper-item {
  width: 100%;
  flex-shrink: 0;
}
.card {
  width: 300px;
  height: 150px;
  border-radius: 12px;
  background-color: #9747FF;
}

Apply snapping:

.swiper {
  scroll-snap-type: x mandatory;
}
.swiper-item {
  scroll-snap-align: center;
  scroll-snap-stop: always; /* prevents fast scrolling from skipping items */
}

Hide the native scrollbar (optional):

::-webkit-scrollbar {
  width: 0;
  height: 0;
}
CSS scroll snap demo
CSS scroll snap demo

2. CSS Scroll‑Driven Indicator Animation

Add a pagination element with three dot placeholders.

<div class="swiper"> ... </div>
<div class="pagination">
  <i class="dot"></i>
  <i class="dot"></i>
  <i class="dot"></i>
</div>

Position the pagination at the bottom centre of the carousel:

.pagination {
  position: absolute;
  display: inline-flex;
  justify-content: center;
  bottom: 10px;
  left: 50%;
  transform: translateX(-50%);
  gap: 4px;
}
.dot {
  width: 6px;
  height: 6px;
  border-radius: 3px;
  background: rgba(255,255,255,0.36);
  transition: 0.3s;
}

Use a pseudo‑element to draw a highlighted dot that will be animated:

.pagination::before {
  content: '';
  position: absolute;
  width: 6px;
  height: 6px;
  border-radius: 3px;
  background-color: #F24822;
  left: 0;
}
.pagination::after {
  animation: move 3s linear forwards;
  animation-timeline: --scroller;
}
@keyframes move {
  to { left: 100%; transform: translateX(-100%); }
}

Link the animation to the scroll container with a scroll‑timeline:

.swiper {
  scroll-timeline: --scroller x;
}
.pagination::after {
  animation: move 3s linear forwards;
  animation-timeline: --scroller;
}

Alternative step‑based animation (e.g., three discrete steps) can be expressed with steps(3, jump-none):

.pagination::after {
  animation: move 3s steps(3, jump-none) forwards;
  animation-timeline: --scroller;
}

3. CSS Timeline‑Scope for Independent Animations

By default, a scroll‑timeline only affects descendant elements. Adding timeline-scope on a common ancestor (e.g., body) expands the scope so that unrelated elements can be driven by the same timeline.

body { timeline-scope: --myScroller; }
.scroller {
  overflow: scroll;
  scroll-timeline-name: --myScroller;
  background: deeppink;
}
.animation {
  animation: rotate-appear;
  animation-timeline: --myScroller;
}

In the swiper example, each slide and its corresponding dot receive a custom variable ( --t1, --t2, --t3) that ties them together:

<div class="swiper-container" style="timeline-scope: --t1, --t2, --t3;">
  <div class="swiper" style="--t: --t1;"> ... </div>
  <div class="swiper" style="--t: --t2;"> ... </div>
  <div class="swiper" style="--t: --t3;"> ... </div>
  <div class="pagination">
    <i class="dot" style="--t: --t1;"></i>
    <i class="dot" style="--t: --t2;"></i>
    <i class="dot" style="--t: --t3;"></i>
  </div>
</div>

Each dot can animate its width and shape at 50 % of the timeline:

@keyframes move {
  50% {
    width: 12px;
    border-radius: 3px 0;
    border-color: rgba(0,0,0,0.12);
    background: #fff;
  }
}

4. Autoplay without JavaScript Timers

Define a dummy animation on the swiper container and listen for the animationiteration event, which fires each time the animation loops (e.g., every 3 s). The handler scrolls the container forward by one viewport width, or resets to the start when the end is reached.

.swiper {
  animation: scroll 3s infinite;
}
@keyframes scroll { to { opacity: .99; } }
swiper.addEventListener("animationiteration", ev => {
  if (ev.target.offsetWidth + ev.target.scrollLeft >= ev.target.scrollWidth) {
    ev.target.scrollTo({ left: 0, behavior: "smooth" });
  } else {
    ev.target.scrollBy({ left: ev.target.offsetWidth, behavior: "smooth" });
  }
});

Pause autoplay on hover (or active state) with animation-play-state: paused:

.swiper:hover, .swiper:active { animation-play-state: paused; }

5. Scroll Callback Events

Because the carousel is native scrolling, the current slide index can be derived from the scroll offset.

swiper.addEventListener("scroll", ev => {
  const index = Math.floor(swiper.scrollLeft / swiper.offsetWidth);
  console.log(index);
});

To emit the index only when it changes, store the previous value and compare:

swiper.addEventListener("scroll", ev => {
  const index = Math.floor(swiper.scrollLeft / swiper.offsetWidth);
  if (swiper.index !== index) {
    swiper.index = index;
    console.log(index);
  }
});

Adding +0.5 makes the change trigger when the scroll passes the halfway point, yielding a more natural slide change:

swiper.addEventListener("scroll", ev => {
  const index = Math.floor(swiper.scrollLeft / swiper.offsetWidth + 0.5);
  if (swiper.index !== index) {
    swiper.index = index;
    console.log(index);
  }
});

In Vue 3 the same logic can be wrapped in a ref and emitted as a custom event:

const current = ref(0);
const scroll = ev => {
  const swiper = ev.target;
  if (swiper) {
    current.value = Math.floor(swiper.scrollLeft / swiper.offsetWidth + 0.5);
  }
};
const emits = defineEmits(['change']);
watch(current, v => emits('change', v));

6. Compatibility Handling

Scroll‑timeline animation is supported in Chrome 115+ only. For browsers that lack support, fall back to a JavaScript‑driven state update that toggles a data-current attribute on the active dot.

swiper.addEventListener("scroll", ev => {
  const index = Math.floor(swiper.scrollLeft / swiper.offsetWidth + 0.5);
  if (swiper.index !== index) {
    swiper.index = index;
    if (!CSS.supports("animation-timeline", "scroll()")) {
      document.querySelector('.dot[data-current="true"]').dataset.current = false;
      document.querySelectorAll('.dot')[index].dataset.current = true;
    }
  }
});

Conditional CSS can be expressed with @supports blocks:

@supports (animation-timeline: scroll()) {
  .dot { animation: move 1s; animation-timeline: var(--t); }
}
@supports not (animation-timeline: scroll()) {
  .dot[data-current="true"] {
    width:12px;
    border-radius:3px 0;
    border-color:rgba(0,0,0,0.12);
    background:#fff;
  }
}

7. Summary

CSS scroll-snap provides simple, performant snapping; scroll-snap-stop: always prevents fast scrolling from skipping slides.

Scroll‑driven animations ( scroll-timeline + animation-timeline) keep pagination indicators in sync with scroll progress. timeline-scope expands the animation influence beyond direct descendants, enabling per‑dot animations tied to individual slides.

Autoplay can be achieved with a dummy CSS animation and the animationiteration event, avoiding traditional JavaScript timers.

Native scroll events allow precise slide‑index calculation; adding +0.5 aligns the index change with the halfway point.

For browsers lacking scroll‑timeline support, use CSS.supports checks and fallback JavaScript that toggles data-current attributes.

All techniques rely only on modern CSS and minimal JavaScript, yielding a lightweight, customizable swiper component.

Reference implementations (code snippets) are hosted at:

CSS swiper demo: https://code.juejin.cn/pen/7391010495207047205

CSS swiper timeline‑scope demo: https://code.juejin.cn/pen/7391018122460954636

CSS swiper autoplay demo: https://code.juejin.cn/pen/739102505507989099

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

CSSscroll-snapscroll-driven animationswiper alternativetimeline-scope
Full-Stack Cultivation Path
Written by

Full-Stack Cultivation Path

Focused on sharing practical tech content about TypeScript, Vue 3, front-end architecture, and source code analysis.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.