Building a Carousel Without JavaScript
Carousels have historically been JavaScript-heavy components, but modern CSS offers a surprisingly clean path to building one without a single line of script. By combining CSS Grid, scroll snapping, and a bit of thoughtful markup, we can create a functional, image-based carousel that works as a progressive enhancement on top of plain HTML.
The Markup: Navigation Plus Gallery
The core structure consists of two parts: a thumbnail navigation strip on the left and a larger gallery view on the right. Each thumbnail links to an anchor ID that corresponds to one of the full-size images in the gallery. This gives us click-to-jump navigation for free — no JavaScript required.
<div class="wrapper">
<nav class="lil-nav"></nav>
<div class="gallery"></div>
</div>
Thumbnails go inside the nav element, each pointing at the ID of its matching gallery image:
<nav class="lil-nav">
<a href="#image-1">
<img class="lil-nav__img" src="..." alt="Yosemite" />
</a>
<a href="#image-2">
<img class="lil-nav__img" src="..." alt="Basketball hoop" />
</a>
<!-- more images go here -->
</nav>
The gallery itself is just a scrollable container holding the same images, each given a unique ID so the anchor links can target them:
<div class="gallery">
<img class="gallery__img" id="image-1" src="..." alt="Yosemite" />
<img class="gallery__img" id="image-2" src="..." alt="Basketball hoop" />
<!-- more images go here -->
</div>
Layout with CSS Grid
We set the parent .wrapper as a grid container, establishing two columns and giving ourselves sensible defaults for the images inside:
img {
display: block;
max-width: 100%;
}
.wrapper {
display: grid;
grid-template-columns: 1fr 5fr;
grid-gap: 20px;
}
Both the thumbnail nav and the gallery need to handle overflow so their content can scroll independently:
.wrapper {
display: grid;
grid-template-columns: 1fr 5fr;
grid-gap: 10px;
overflow: hidden;
height: 100vh;
}
.gallery {
overflow: scroll;
}
.lil-nav {
overflow-y: scroll;
overflow-x: hidden;
}
Scroll Snapping for a Carousel Feel
To make the gallery stop neatly on each image rather than free-scrolling, we apply scroll-snap-type on the container and scroll-snap-align on the images:
.gallery {
overflow: scroll;
scroll-snap-type: x mandatory;
}
.gallery__img {
scroll-snap-align: start;
margin-bottom: 10px;
}
That single pair of properties turns a simple scroll area into something that behaves like a traditional carousel, snapping cleanly from slide to slide.
Sprucing Up the Images
Right now the gallery images only take up as much space as their natural dimensions. Using object-fit along with a min-height based on viewport height (vh) ensures each image fills the screen regardless of its aspect ratio:
.gallery__img {
scroll-snap-align: start;
margin-bottom: 10px;
min-height: 100vh;
object-fit: cover;
}
The thumbnail strip benefits from similar treatment. A grayscale filter on the inactive thumbnails, removed on hover, gives visual feedback without any scripting:
.lil-nav {
overflow-y: scroll;
overflow-x: hidden;
}
.lil-nav a {
height: 200px;
display: flex;
margin-bottom: 10px;
}
.lil-nav__img {
object-fit: cover;
}
.lil-nav__img {
object-fit: cover;
filter: saturate(0);
transition: 0.3s ease all;
}
.lil-nav__img:hover {
transform: scale(1.05);
filter: saturate(1);
}
Smooth Scrolling Between Slides
Clicking a thumbnail currently jumps abruptly to the matching gallery image. Adding the scroll-behavior property to the scrollable container animates that transition smoothly:
.gallery {
overflow: scroll;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
}
Thumbnails, for what it's worth, keep their default scroll behavior — turning them into another snapping carousel felt unnatural in testing.
Making It Responsive
The layout shifts from a side-by-side arrangement on desktop to a stacked one on mobile. Wrapping the desktop styles in a media query that targets larger screens, and using Sass to keep things organized, handles the breakpoint:
$large: 1200px;
.wrapper {
overflow: hidden;
height: 100vh;
display: grid;
grid-template-rows: 2fr 1fr;
grid-gap: 10px;
@media screen and (min-width: $large) {
grid-template-columns: 1fr 5fr;
grid-template-rows: auto;
}
}
The nav moves to the second row on smaller screens so it sits below the gallery:
.lil-nav {
overflow-x: scroll;
overflow-y: hidden;
display: flex;
grid-row-start: 2;
@media screen and (min-width: $large) {
overflow-y: scroll;
overflow-x: hidden;
display: block;
grid-row-start: auto;
}
}
The gallery's scroll-snap type and overflow direction also flip depending on screen size:
.gallery {
overflow-x: scroll;
overflow-y: hidden;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
display: flex;
@media screen and (min-width: $large) {
display: block;
overflow-y: scroll;
overflow-x: hidden;
scroll-snap-type: y mandatory;
}
}
Final Notes on Production Readiness
This approach gives us a genuinely usable carousel with only HTML and CSS. For a production deployment, two areas deserve attention: accessibility — screen readers likely shouldn't announce every image twice (once in the nav, once in the gallery) — and performance, where lazy-loading gallery images would prevent unnecessary downloads. Those refinements aside, the technique stands on its own as a lightweight alternative to the usual JavaScript-heavy approaches.



