.container {
display: flex; /* place slides in a row */
overflow: auto; /* make container scrollable */
position: relative; /* this will come in handy later */
}
.slide {
width: 100%; /* make slides responsive */
flex-shrink: 0; /* prevent shrinking */
}
Carousels
You don't have to look far on the internet to come across tabs or carousels.
Almost any website I work on these days includes at least one of them. I've seen dozens of implementations, and yet there always seems to be something missing.
So I decided to give it a go myself.
The goal here isn't to replace the numerous carousel and tab libraries out there.
Instead, I'd just like to explore what's possible using some basic CSS and JavaScript.
The Key Idea
We can get scrollable, responsive, and animated Tabs with just 5 lines of CSS:
This doesn't give us the tab triggers out of the box, but we do get a solid foundation to build upon.
We already have a natively scrollable container and responsive slides before touching javascript. (try scrolling to the right)
What we need
- A list of slides and triggers
- Clicking a trigger should scroll to the appropriate slide
<div class="triggers">
<button onclick="scrollTab(slide_1)">...
<button onclick="scrollTab(slide_2)">...
</div>
<div class="container">
<div class="slide">...
<div class="slide">...
</div>
How do we go about finding the correct scroll position?
Scroll Position
Rather than thinking of the content moving, we can reframe the idea of scrolling as moving a viewport across a fixed strip of content.
Turns out, the scroll position of a slide is just the amount of space to it's left.
You could probably spot the formula index * (width + gap)
This will work, but it means that we either need to use a fixed width and gap, or we need to calculate the widths and gaps.
(and make sure they stay up-to-date as the screen is resized)
A better way
Fortunately for us, there's a more direct way to get the scroll position without dealing with widths and gaps.
The offsetLeft
property
This property returns the number of pixels that the upper left corner of the current element is offset to the left within it's parent.*
* This is only true if we have position:relative
on the parent. Otherwise, offsetLeft
returns the distance to the edge of the screen
Because display: flex
places our items in a row, a slide's offsetLeft
value is exactly equal to it's scroll position. (even with the overflow)
const scrollTab = (slide) => {
container.scrollTo({
left: slide.offsetLeft,
behavior: "smooth",
});
};
That's all we need to implement the triggers.
Adding behavior: "smooth"
nicely animates the scroll without us having to do
anything else.
Also, because we calculate offsetLeft
the moment a trigger is clicked, it will always
be up-to-date.
Options & Enhancements
We can snap slides into place using the scroll-snap-type
property.
.container {
display: flex;
overflow: auto; /* allow scrolling */
scroll-snap-type: "x mandatory"; /* snap to slides */
}
.slide {
width: 100%;
flex-shrink: 0;
scroll-snap-align: start; /* set snapping point */
}
We've actually been using this property all along, but I didn't mention it earlier because it's a little bit buggy out-of-the-box :(
scroll-snap
works pretty well when scrolling, but there's some jank when using the triggers.
To get around this, we temporarily disable snapping when a trigger is clicked, and re-enable it after the finishes.
Next, we can set overflow: hidden
to prevent scrolling and only allow navigation with
the triggers.
overflow:
auto
hidden
In general, it's a good idea to allow scrolling. This way, users can navigate using their preferred method.
Finally, you'll also notice the correct trigger highlights as we scroll. This is done using the Intersection Observer API.
Bloopers
I would have loved to implement a version without JavaScript, using anchor tags as triggers — <a href="#slide-id"...
(which I did).
However, it's a bit limited:
- Most browsers first scroll the entire page to the anchor, then to the correct slide. In some cases, this behavior might be desired, but it's not ideal for a carousel.
- The slides don’t always line up correctly (you may notice the border being cut off).
I think we need at least some JavaScript for a fully functional carousel. Nonetheless, I’ve shared the implementation below if you’d like to experiment with it.
That's a wrap!
I hope you learned something from this Craft. I had a lot of fun putting it together :)
Digging into this UI pattern and experimenting with different approaches has given me a new appreciation for what goes into making a proper tabs or carousel library.
I'm fairly new to making these Craft pages, but I'm excited to share more of my experiments with you down the road.
If you have any feedback, suggestions, or just wanna chat, feel free to reach out!