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.
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;
}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
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Full-Stack Cultivation Path
Focused on sharing practical tech content about TypeScript, Vue 3, front-end architecture, and source code analysis.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
