Horizontal-scroll card carousel where each Nth card has a slight rotation, creating an authentic "photos pinned to a board" feel. Tilts cancel on hover so users can read clearly. Pairs scroll-snap with edge-mask for cinematic fade-out at the rails.
Scroll horizontally. Hover a card to see the tilt cancel + slight lift.
<section class="ugc-section">
<div class="ugc-track-wrap">
<div class="ugc-track">
<div class="ugc-card">
<div class="ugc-img"><img src="customer-1.jpg"></div>
<div class="ugc-cap"><b>@laura · Madrid</b>"En el jardín..."</div>
</div>
<div class="ugc-card">...</div>
<!-- 6-12 more cards -->
</div>
</div>
</section>
.ugc-track-wrap {
position: relative;
/* Edge mask: fade out at the left + right rails (5% / 95% stops) */
-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 5%, #000 95%, transparent 100%);
mask-image: linear-gradient(90deg, transparent 0, #000 5%, #000 95%, transparent 100%);
}
.ugc-track {
display: flex; gap: 22px;
overflow-x: auto; overflow-y: hidden;
scroll-snap-type: x mandatory;
scrollbar-width: none;
padding: 18px 24px 24px;
}
.ugc-track::-webkit-scrollbar { display: none; }
.ugc-card {
flex: 0 0 auto;
width: clamp(220px, 26vw, 290px);
scroll-snap-align: start;
transition: transform 400ms cubic-bezier(0.4, 0, 0.2, 1);
}
/* THE TILTS — :nth-child rotates each Nth card differently */
.ugc-card:nth-child(4n+1) { transform: rotate(-1.4deg); }
.ugc-card:nth-child(4n+2) { transform: rotate(0.9deg) translateY(8px); }
.ugc-card:nth-child(4n+3) { transform: rotate(-0.6deg) translateY(-4px); }
.ugc-card:nth-child(4n+4) { transform: rotate(1.6deg); }
/* On hover: cancel tilt, lift, raise above siblings */
.ugc-card:hover {
transform: rotate(0) translateY(-4px) scale(1.02);
z-index: 2;
}
/* Mobile — drop the tilts (reads as broken on small screens) */
@media (max-width: 720px) {
.ugc-card:nth-child(4n+1),
.ugc-card:nth-child(4n+2),
.ugc-card:nth-child(4n+3),
.ugc-card:nth-child(4n+4) { transform: rotate(0); }
}
Tilt magnitude. 1–2 degrees is the sweet spot. Below 1° you don't notice it; above 2° it starts looking accidental rather than intentional. The asymmetric translateY on every other card (8px / -4px) adds the "loosely arranged" feel.
The :nth-child(4n+N) cycle. Using a 4-cycle means patterns repeat every 4 cards — visually feels random because 4 different tilts in sequence aren't obvious. A 2-cycle reads as zebra-stripe; an 8-cycle is overkill.
Mobile cancel. Tilts on small screens crowd
the cards into each other (especially with horizontal scroll), making it
hard to read. Always reset to rotate(0) below ~720px.
Hover lift. The z-index: 2 is critical — without it, the lifted card gets clipped by neighbors during the 400ms transition.
Edge mask only works on opaque backgrounds. The
mask-image creates a gradient that fades elements out at the
edges. Looks great on a solid section bg, breaks if the parent has a
patterned background that's supposed to show through.
scroll-snap fights with the tilts initially. If you
set scroll-snap-align: start on a tilted card, the snap
position will be at the visual leading edge of the tilted bounding box,
which can look off-center. Usually fine, but worth checking on the
first card.
Don't tilt the captions separately. Caption text inherits the parent transform, which is what you want — the caption rotates WITH the photo above it. Trying to counter-rotate the caption feels uncanny.
Hover-cancel transition timing. 400ms cubic-bezier ease-out is calm. Faster (200ms) feels jumpy; slower (700ms) feels sluggish.
Source: Mithjem homepage UGC section ("Hemos dado vida a más de 15.000 jardines"), 2026-05-23+. Replaces the "uniform grid" UGC pattern that felt too commercial.