Carousel with Scroll Controls
anchor-positioning
scroll-snap
Native carousel using scroll-snap with anchor-positioned controls and scroll markers.
Carousel Structure
Set anchor-name
for button positioning and scroll-marker-group: after
for marker placement.
<ul class="carousel">
<li>
<figure>
<img src="/images/jeremy-hynes-zLrqHNms8eE-unsplash.jpg" alt="">
<figcaption>
Photo by <a target="_parent" href="https://unsplash.com/@hynesight?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Jeremy Hynes</a> on <a target="_parent" href="https://unsplash.com/photos/brown-owl-on-tree-branch-covered-with-snow-zLrqHNms8eE?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>
</figcaption>
</figure>
</li>
<li>
<figure>
<img src="/images/richard-jacobs-8oenpCXktqQ-unsplash.jpg" alt="">
<figcaption>
Photo by <a target="_parent" href="https://unsplash.com/@rj2747?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Richard Jacobs</a> on <a target="_parent" href="https://unsplash.com/photos/grayscale-photo-of-elephants-drinking-water-8oenpCXktqQ?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>
</figcaption>
</figure>
</li>
<li>
<figure>
<img src="/images/rod-long-gUYYvPrnuHY-unsplash.jpg" alt="">
<figcaption>
Photo by <a target="_parent" href="https://unsplash.com/@rodlong?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Rod Long</a> on <a target="_parent" href="https://unsplash.com/photos/black-whale-in-water-during-daytime-gUYYvPrnuHY?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>
</figcaption>
</figure>
</li>
</ul>
.carousel {
container: inline-size;
display: flex;
gap: 20px;
overflow-x: scroll;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
list-style: none;
position: relative;
anchor-name: --carousel;
scroll-marker-group: after;
scrollbar-width: none;
}
.carousel li {
flex: 0 0 100%;
scroll-snap-align: center;
container-type: scroll-state;
@supports (container-type: scroll-state) {
@media (prefers-reduced-motion: no-preference) {
figcaption {
transition: transform 0.3s ease-in-out;
transform: translateY(100%);
@container scroll-state(snapped: x) {
transform: translateY(0);
}
}
}
}
}
figure {
aspect-ratio: 16 / 9;
display: grid;
place-items: center;
font-size: 3rem;
font-weight: bold;
border-radius: 8px;
overflow: hidden;
position: relative;
}
figure figcaption {
position: absolute;
bottom: 0;
width: 100%;
background-image: linear-gradient(to top, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0));
color: white;
padding: 40px 10px 10px;
font-size: 0.8rem;
text-align: left;
font-weight: lighter;
}
figcaption a {
color: white;
}
Scroll Button Controls
::scroll-button
pseudo-elements anchored to carousel using position-anchor
with directional positioning.
.carousel::scroll-button(*) {
font-family: "Material Icons";
position: absolute;
position-anchor: --carousel;
/* make them round and easy to press */
inline-size: 48px;
aspect-ratio: 1;
border-radius: 9999px;
background-color: #fff;
font-size: 20px;
border: 1px solid #999;
transition: all 0.1s ease-in-out;
}
.carousel::scroll-button(*):focus-visible {
outline-offset: 4px;
}
.carousel::scroll-button(*):disabled {
opacity: 0.3;
}
.carousel::scroll-button(*):not(:disabled):is(:hover, :active) {
background-color: rgba(255, 255, 255, 0.8);
}
.carousel::scroll-button(*):not(:disabled):active {
scale: 95%;
transform-origin: center;
}
.carousel::scroll-button(left) {
content: 'chevron_left' / 'Previous';
position-area: center;
translate: calc(var(--carousel-item-half-width) * -1 + 36px);
}
.carousel::scroll-button(right) {
content: 'chevron_right' / 'Next';
position-area: center;
translate: calc(var(--carousel-item-half-width) - 36px);
}
Marker Group Layout
::scroll-marker-group
creates auto-flow grid container with scroll handling for marker overflow.
.carousel::scroll-marker-group {
grid-area: markers; /* place markers in parent grid area */
/* 15px by 15px horizontal grid - size of dots */
display: grid;
place-content: safe center;
grid: 12px / auto-flow 12px;
gap: 20px;
padding: 15px;
scroll-padding: 15px;
/* handle overflow */
overflow: auto;
overscroll-behavior-x: contain;
scrollbar-width: none;
scroll-snap-type: x mandatory;
@media (prefers-reduced-motion: no-preference) {
scroll-behavior: smooth;
}
}
Individual Markers
::scroll-marker
styles each indicator with :target-current
highlighting the active slide.
.carousel li::scroll-marker {
content: ''/ attr(data-name);
width: 10px;
height: 10px;
border-radius: 9999px;
background-color: white;
border: 1px solid black;
/* snap if group is overflowing */
scroll-snap-align: center;
}
.carousel li::scroll-marker:is(:hover, :focus-visible) {
border-color: gray;
}
.carousel li::scroll-marker:target-current {
background: black;
border-color: black;
}