GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

9 CSS Only Carousel Slider Recipes to Copy-Paste

A CSS only carousel slider is a row of photos or cards that steps through one at a time in the same frame, built nine ways here — scroll snap to radio hack,

Auto-plays · click a tile to jump to its section · all nine in one zip

The order isn't a popularity ranking — it follows how a carousel actually gets built. 01 is the plainest version, a strip you can just drag. 02 through 04 add ways to jump straight to one slide — arrows, dots, thumbnails — while staying script-free. 05 and 06 change the shape of the same idea, one turning the axis vertical, the other tilting every card in 3D. 07 through 09 close with three that don't need anyone to touch them at all: a card stack that swipes itself, a zooming hero, and a bar that reads the scroll position directly. A companion that travels well with a carousel is the back to top button in the corner of the screen, and a layer pinned above a strip of slides is the sticky header variants.

01Peek-next scroll-snap

Why the next card peeks

Each card leaves enough side padding that the next slide peeks into view at the edge of the frame, and scroll-padding-left keeps the first card snapped flush against the left side instead of drifting under the container's own edge.

Peek-next scroll-snap
Open in a new tab

01 · Peek-next scroll-snap — Measured area 37.5% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

scroll-paddingscroll-snap-alignJS 0줄

Where it fits

It suits a shop's product row or a short row of recommended reads, where a visitor should sense there's more without an arrow having to say so.

.pn__viewport {
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-left: 16px;
}
.pn__track { display: flex; width: max-content; }
.pn__slide {
  flex: 0 0 200px;
  scroll-snap-align: start;
  margin-right: 12px;
}

02Radio + arrow slider

Three radios and two labels

Three hidden radio inputs and a pair of arrow-shaped labels are the whole mechanism here — checking one radio doesn't just toggle a class on its own slide — it shifts the entire track sideways by a third through a sibling selector.

Radio + arrow slider
Open in a new tab

02 · Radio + arrow slider — Measured area 50.374% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

:checked형제결합자label for

The oldest css only carousel with arrows

It's the oldest no-scroll, no-JS way to build a carousel, and it still suits a banner or a notice card someone flips through by hand only.

.ra__viewport { overflow: hidden; }
.ra__track {
  display: flex;
  width: 300%;
  transition: transform 300ms;
}
#ra-1:checked ~ .ra__viewport .ra__track { transform: translateX(0%); }
#ra-2:checked ~ .ra__viewport .ra__track { transform: translateX(-33.3333%); }
#ra-3:checked ~ .ra__viewport .ra__track { transform: translateX(-66.6667%); }

03Dot nav via :target

A dot click is a real link

Clicking a dot is a real link click, nothing more — the address bar's # fragment changes and the browser scrolls that slide into view inside the snap track entirely on its own.

Dot nav via :target
Open in a new tab

03 · Dot nav via :target — Measured area 35.13% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

:targetscroll-margin앵커 링크

Why the back button works

:target only steps in afterward, to color whichever dot matches the slide now showing, which is why the back button correctly returns to the previous slide.

.dt__viewport {
  overflow-x: auto;
  scroll-snap-type: x mandatory;
}
.dt__slide { scroll-snap-align: start; }
.dt:has(#dt-s1:target) .dt__dot--1,
.dt:has(#dt-s2:target) .dt__dot--2,
.dt:has(#dt-s3:target) .dt__dot--3 {
  background: var(--color);
  transform: scale(1.4);
}

04Thumbnail-synced snap

A css only image slider needs real thumbnails

A real strip of square thumbnail images sits below the main photo, each one a plain anchor pointing at its matching slide, so the same :has() trick from the dots above now outlines a thumbnail instead of coloring a dot.

Thumbnail-synced snap
Open in a new tab

04 · Thumbnail-synced snap — Measured area 26.853% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

:target형제결합자scroll-snap-align

Alt text for every frame

Because these are actual image elements rather than generated markers, each one can carry its own alt text for a product-detail gallery.

.tm__thumb {
  border: 3px solid transparent;
  transition: border-color 300ms;
}
.tm:has(#tm-s1:target) .tm__thumb--1,
.tm:has(#tm-s2:target) .tm__thumb--2,
.tm:has(#tm-s3:target) .tm__thumb--3 {
  border-color: #17141a;
}

05Vertical snap

Turning the axis vertical

Swap the scroll axis from horizontal to vertical and scroll-snap keeps working exactly the same way, so each slide fills the box top to bottom instead of side to side — the same direction a phone's story viewer scrolls in.

Vertical snap
Open in a new tab

05 · Vertical snap — Measured area 18.622% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

scroll-snap-type: yscroll-snap-align100%

Where a vertical strip reads better

A vertical story banner or a mobile full-screen intro reads naturally in this direction where a sideways strip wouldn't.

.vs__viewport {
  height: 100%;
  overflow-y: auto;
  scroll-snap-type: y mandatory;
}
.vs__track { display: flex; flex-direction: column; }
.vs__slide { flex: 0 0 150px; scroll-snap-align: start; }

06Cover-flow snap

No radio decides the front card

No radio input picks which card faces forward — real horizontal dragging is the only control, and every card is already tilted with a fixed rotateY and scale set purely by its position in the row.

Cover-flow snap
Open in a new tab

06 · Cover-flow snap — Measured area 31.972% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

perspectivescroll-snap-align: centerrotateY

Why the center looks centered

Whichever card scrolls itself to the center happens to be the one sitting at rotateY(0deg), so it just looks centered without any state ever changing.

.cf__viewport {
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  perspective: 640px;
}
.cf__slide { scroll-snap-align: center; }
.cf__slide--1, .cf__slide--5 { transform: rotateY(38deg) scale(.72); }
.cf__slide--2, .cf__slide--4 { transform: rotateY(24deg) scale(.86); }
.cf__slide--3 { transform: rotateY(0deg) scale(1); }

07Stacked card swipe

Three cards in one grid cell

Three cards sit stacked in the exact same grid cell with no scroll box anywhere in sight, and all three play the identical @keyframes loop — only animation-delay staggers when each one starts, which alone is enough to make the top card peel off sideways while the next one rises to take its spot.

Stacked card swipe
Open in a new tab

07 · Stacked card swipe — Measured area 41.574% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

grid-areatransformz-index

Pausing the stack

Hovering the stack is the only way to pause it, via animation-play-state: paused.

.cs__card {
  grid-area: 1 / 1;
  animation: cs-cycle 6s ease-in-out infinite;
}
.cs__card--2 { animation-delay: -.667s; }
.cs__card--3 { animation-delay: -1.333s; }
@keyframes cs-cycle {
  0%, 4%   { transform: translate(0, 0); opacity: 1; z-index: 3; }
  29%      { transform: translate(42%, 6%) rotate(12deg); opacity: 0; }
  33%, 62% { transform: translate(-6%, 6%) rotate(-4deg); opacity: .85; }
  66%, 96% { transform: translate(6%, 4%) rotate(3deg); opacity: .92; }
  100%     { transform: translate(0, 0); opacity: 1; z-index: 3; }
}

08Autoplay hero with Ken Burns zoom

Cross-fade plus a private zoom

Three photos cross-fade on their own timer, and the one currently on screen also runs its own private zoom — scale climbs from 1 to 1.16 only for as long as that photo is visible, then resets before its next turn comes around.

Autoplay hero with Ken Burns zoom
Open in a new tab

08 · Autoplay hero with Ken Burns zoom — Measured area 56.128% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · See the password

transform: scale@keyframesopacity

What keeps a still hero alive

That extra layer is what keeps a still hero banner feeling alive instead of just cutting between flat frames.

.kb__card { grid-area: 1 / 1; animation: kb-fade 6s linear infinite; }
.kb__img { animation: kb-zoom 6s ease-out infinite; }
@keyframes kb-fade { 0%, 4% { opacity: 1; } 29%, 100% { opacity: 0; } }
@keyframes kb-zoom {
  0%   { transform: scale(1); }
  29%  { transform: scale(1.16); }
  29.1%, 100% { transform: scale(1); }
}

09Scroll-linked progress bar

Reading the scroll position directly

No radio hack and no JavaScript reading a scroll offset — animation-timeline points a keyframe animation at a named scroll-timeline and lets the real horizontal scroll position fill the bar's width directly, frame for frame.

Scroll-linked progress bar
Open in a new tab

09 · Scroll-linked progress bar — Measured area 36.869% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

animation-timeline: scroll()scroll-timelineJS 0줄

What the render measured

Measuring the render showed 23 of the 24 captured frames moving, more than any other recipe in this set, because the bar tracks the scroll continuously instead of jumping between fixed states.

.sp { timeline-scope: --sp-progress; }
.sp__viewport { scroll-timeline: --sp-progress inline; }
.sp__fill {
  animation-name: sp-fill;
  animation-timeline: --sp-progress;
  animation-fill-mode: both;
}
@keyframes sp-fill { from { width: 0%; } to { width: 100%; } }

Where does this break?

A sibling cannot see the timeline

Item 09's progress bar shipped broken the first time it was wired up. The bar (.sp__bar) and the scroll box (.sp__viewport) are siblings, not parent and child, and a scroll-timeline-name declared on one element is only visible to that element's own descendants by default — a sibling can't see it at all. The bar sat frozen at 0% width no matter how far the slides scrolled, because animation-timeline: --sp-progress on the fill had nothing to attach to.

What timeline-scope opens up

Declaring timeline-scope: --sp-progress on their shared parent is what opens that name up to the sibling side of the tree, and only after adding it did the bar start filling in step with the real scroll. Rendering it in an installed Chrome build confirmed the fix directly, frame by frame, from empty to about half full to completely filled; a browser that hasn't shipped animation-timeline yet just leaves the bar empty while scrolling and snapping keep working normally. Every one of the nine folders got re-checked this way before the zip was sealed, and typing b2earrkf exactly where the download page shows it unlocks the corrected set. A cover stacked on a cover is the same z-order trouble the cookie consent banner patterns run into, with a policy dialog buried under the banner.

Variants worth trying

Variant Changed value Feel
Slower radio flip Item 02's track transition stretched from 300ms to 500ms The panel change reads as a deliberate page turn instead of a quick swap
Flatter cover flow Item 06's side-card angles cut from 38deg/24deg to 20deg/10deg Neighboring covers stay mostly readable instead of nearly edge-on
Eased progress fill Item 09's animation-timeline keyframe swapped from linear to ease-out The bar rushes early in the scroll and settles into the last stretch, instead of moving at one constant rate

Accessibility

Does a css only carousel autoplay still move under reduced motion?

Under prefers-reduced-motion: reduce, every item's own demo-preview loop turns off with animation: none !important, leaving only what a real drag, click, radio change, or anchor jump still causes. Items 07 and 08 stop mid-cycle and lock onto their first card so nothing keeps drifting on its own; the scroll-snap items (01, 03, 04, 05, 06, 09) simply set scroll-behavior: auto, so Tab and the arrow keys still move focus one slide at a time, just without an eased glide. MDN's prefers-reduced-motion reference lists exactly which platform settings trigger it.

Keyboard and screen reader paths

Item 02's radio group is a native form control, so arrow keys already switch slides with no extra markup at all. The dots in item 03 and the thumbnails in item 04 each carry an aria-label="Slide N" so a screen reader can tell them apart even though they render with no visible text.

Where to go next

None of the nine needs a 3D-tilted radio state or a ::scroll-marker to work — for that route through the same idea, see 9 CSS-Only Carousels You Can Copy-Paste, a separate set built around the browser-generated scroll markers this one deliberately avoids. More scroll-driven CSS sits in the CSS category hub, and MDN's scroll-snap-type entry covers exactly how snapping chooses a stopping point.

FAQ

Do any of these nine need JavaScript to actually run?

No — every slide change comes from native scrolling, a radio input's :checked state, or a plain anchor link's :target. The only script in each demo file is a three-line listener that stops the auto-playing preview loop once a visitor drags or clicks; deleting it removes nothing but that preview.

What is the css only carousel browser support for the item 09 progress bar?

It was measured in an installed Chrome build, where animation-timeline and timeline-scope are supported and the fill was confirmed moving frame by frame. Where those properties aren't supported yet, the bar just stays empty — the slides underneath still scroll and snap normally, so nothing about navigating the carousel breaks.

How different is the React version underneath?

Items 01, 05, 06, 07, 08, and 09 carry the same scroll-snap rules, keyframes, and animation-timeline straight into a CSS module, so the mechanism is identical. Item 02 swaps its radio group for one useState index, and item 03 (and 04 the same way) replaces :target with a scrollIntoView call so multiple carousel instances on one page never collide over the same anchor id; duration, easing, and color all become props either way. For two photos compared inside one frame, the before after image sliders hand over the handle.

Enter the archive password

The password is inside this article. You will find it as you read.