← Visual Library / Layouts

Tilted UGC scatter cards

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.

Layouts Built
Use when — you're showing user-generated content (real customer photos, social posts, screenshot collages) and want it to feel organic and human, not clinically gridded. The slight rotations signal "these are real photos people sent us" without sliding into kitsch. Best with 6–12 cards in horizontal scroll.

Live demo

Scroll horizontally. Hover a card to see the tilt cancel + slight lift.

photo 1
@laura · Madrid"En el jardín nunca se vio mejor."
photo 2
@carlos · Sevilla"Llega a todas las esquinas."
photo 3
@ana · Valencia"2 años y sigue intacta."
photo 4
@miguel · Barcelona"Los niños la usan a diario."
photo 5
@javier · Bilbao"Otra liga frente a las baratas."
photo 6
@patricia · Granada"30m llegan hasta el huerto."
photo 7
@elena · Málaga"Compacta y ligera, perfecta."
photo 8
@toni · Bilbao"Recomendadísima."

HTML

<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>

CSS — the whole pattern

.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); }
}

Dials

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.

Gotchas worth knowing

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.

Pairs well with

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.