Advanced
Cards
Build an iMessage-style card stack.
.carousel {
--card-width: 12rem;
display: grid;
grid-auto-flow: column;
grid-auto-columns: 100%;
scroll-snap-type: x mandatory;
width: calc(var(--card-width) * 3);
padding-inline: var(--card-width);
}
.slide {
width: var(--card-width);
aspect-ratio: 3 / 4;
position: sticky;
left: calc(var(--card-width) * -1);
right: calc(var(--card-width) * -1);
scroll-snap-align: center;
scroll-snap-stop: always;
transform-origin: center 70%;
will-change: transform;
view-timeline: --cards inline;
animation: stack-cards linear both;
animation-timeline: --cards;
animation-range: contain;
& .card {
width: 100%;
height: 100%;
animation: rotate-cards linear both;
animation-timeline: --cards;
animation-range: contain -50% contain 150%;
}
}
@keyframes stack-cards {
0% {
z-index: calc(100 - sibling-index());
}
40% {
z-index: 1000;
}
100% {
z-index: sibling-index();
}
}
@keyframes rotate-cards {
0% {
transform: translateX(-80%) rotate(10deg) scale(0.8);
}
25% {
transform: translateX(-90%) rotate(5deg) scale(0.9);
}
50% {
transform: translateX(0%) rotate(0deg) scale(1);
}
60% {
transform: translateX(-20%) rotate(-15deg) scale(0.6);
}
75% {
transform: translateX(90%) rotate(-5deg) scale(0.9);
}
100% {
transform: translateX(80%) rotate(-10deg) scale(0.8);
}
}
<BlossomCarousel class="carousel">
<div class="slide">
<div class="card"></div>
</div>
<div class="slide">
<div class="card"></div>
</div>
<div class="slide">
<div class="card"></div>
</div>
...
</BlossomCarousel>
carousel.addEventListener("overscroll", (event: CustomEvent) => {
const overscroll = event.detail.left * 0.2;
Array.from(event.target.children).forEach((child) => {
child.style.transform = `translateX(${overscroll}px) rotate(${(overscroll / event.target.clientWidth) * 70}deg) scale(${1 - Math.abs(overscroll) / event.target.clientWidth})`;
});
});
.carousel {
--card-width: 12rem;
}
.slide {
transform-origin: center 70%;
will-change: transform;
view-timeline: --cards inline;
animation: stack-cards linear both;
animation-timeline: --cards;
animation-range: contain;
& .card {
animation: rotate-cards linear both;
animation-timeline: --cards;
animation-range: contain -50% contain 150%;
}
}
@keyframes stack-cards {
0% {
z-index: calc(100 - sibling-index());
}
40% {
z-index: 1000;
}
100% {
z-index: sibling-index();
}
}
@keyframes rotate-cards {
0% {
transform: translateX(-80%) rotate(10deg) scale(0.8);
}
25% {
transform: translateX(-90%) rotate(5deg) scale(0.9);
}
50% {
transform: translateX(0%) rotate(0deg) scale(1);
}
60% {
transform: translateX(-20%) rotate(-15deg) scale(0.6);
}
75% {
transform: translateX(90%) rotate(-5deg) scale(0.9);
}
100% {
transform: translateX(80%) rotate(-10deg) scale(0.8);
}
}
<BlossomCarousel
class="carousel grid! snap-x snap-mandatory auto-cols-[100%] grid-flow-col w-[calc(var(--card-width)*3)]! px-(--card-width)"
>
<div
v-for="i in 10"
:key="i"
class="slide sticky -left-(--card-width) -right-(--card-width) w-(--card-width) aspect-[3/4] snap-center snap-always"
>
<div class="card size-full">{{ i }}</div>
</div>
</BlossomCarousel>
carousel.addEventListener("overscroll", (event: CustomEvent) => {
const overscroll = event.detail.left * 0.2;
Array.from(event.target.children).forEach((child) => {
child.style.transform = `translateX(${overscroll}px) rotate(${(overscroll / event.target.clientWidth) * 70}deg) scale(${1 - Math.abs(overscroll) / event.target.clientWidth})`;
});
});