April 2024
My attempt at tackling this common design pattern.
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.
We can get scrollable, responsive, and animated Tabs with just 5 lines of CSS:
.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 */
}
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)
<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?
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)
Fortunately for us, there's a more direct way to get the scroll position without dealing with widths and gaps.
offsetLeft
propertyThis 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.
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.
I would've loved to implement a version with no javascript by using anchor tags for the triggers — <a href="#slide-id">...
(and I did).
Unfortunately, it's extremely buggy:
scroll-behavior: smooth
.Nonetheless, I've shared the implementation below if you'd like to play around with it.
If anyone knows why this is happening please let me know!. I'd love to revisit this idea in the future.
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 or suggestions, or just wanna chat, feel free to reach out!