16 CSS Image Gallery Designs 04 / 16

CSS Polaroid Stack Gallery

Five vintage-style polaroid photos stacked with random rotations that fan out on hover, with a flip animation triggered on click.

CSS + JS MIT licensed
Live Demo Open in tab
Open in playground

The code

<div class="ig-04">
  <div class="ig-04__scene">
    <div class="ig-04__stack" id="ig-04-stack">
      <!-- Polaroid 1: Venice canal -->
      <div class="ig-04__card">
        <div class="ig-04__photo">
          <svg viewBox="0 0 150 150" xmlns="http://www.w3.org/2000/svg">
            <defs><linearGradient id="vg" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#6aaad4"/><stop offset="50%" stop-color="#3a88b8"/><stop offset="100%" stop-color="#2a6090"/></linearGradient></defs>
            <rect width="150" height="150" fill="url(#vg)"/>
            <!-- Buildings -->
            <g fill="#c8a870"><rect x="5" y="60" width="25" height="90"/><rect x="33" y="45" width="22" height="105"/><rect x="58" y="55" width="20" height="95"/><rect x="82" y="40" width="28" height="110"/><rect x="113" y="52" width="24" height="98"/></g>
            <g fill="#b89858"><rect x="5" y="60" width="25" height="10"/><rect x="33" y="45" width="22" height="10"/><rect x="58" y="55" width="20" height="10"/><rect x="82" y="40" width="28" height="10"/><rect x="113" y="52" width="24" height="10"/></g>
            <!-- Windows -->
            <g fill="#6888aa" opacity=".7"><rect x="10" y="72" width="6" height="8"/><rect x="20" y="72" width="6" height="8"/><rect x="38" y="58" width="6" height="8"/><rect x="48" y="58" width="6" height="8"/><rect x="88" y="52" width="7" height="10"/><rect x="100" y="52" width="7" height="10"/><rect x="118" y="62" width="7" height="8"/></g>
            <!-- Canal water -->
            <path d="M0,118 Q38,110 75,118 Q112,110 150,118 L150,150 L0,150 Z" fill="#1a4a7a"/>
            <!-- Gondola -->
            <g transform="translate(50,128)">
              <path d="M-25,0 Q-20,-6 0,-5 Q20,-6 28,0 Q22,7 -20,7 Z" fill="#0a0a0a"/>
              <rect x="-5" y="-15" width="2.5" height="15" fill="#8a6030"/>
              <ellipse cx="-4" cy="-16" rx="8" ry="4" fill="#c8a040" opacity=".7"/>
              <ellipse cx="-4" cy="-17" rx="5" ry="3" fill="#e8c060"/>
              <!-- Gondolier -->
              <ellipse cx="10" cy="-8" rx="4" ry="5" fill="#1a1a2a"/>
              <circle cx="10" cy="-14" r="3.5" fill="#c8a880"/>
            </g>
            <!-- Reflection -->
            <g opacity=".3"><rect x="5" y="120" width="25" height="30" fill="#c8a870"/><rect x="33" y="115" width="22" height="35" fill="#c8a870"/></g>
          </svg>
        </div>
        <div class="ig-04__meta">Venice, 1998</div>
      </div>
      <!-- Polaroid 2: Taj Mahal -->
      <div class="ig-04__card">
        <div class="ig-04__photo">
          <svg viewBox="0 0 150 150" xmlns="http://www.w3.org/2000/svg">
            <defs><linearGradient id="tmg" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#f5e8d8"/><stop offset="50%" stop-color="#e8d0b0"/><stop offset="100%" stop-color="#d4b890"/></linearGradient></defs>
            <rect width="150" height="150" fill="#c8e8f8"/>
            <!-- Sky -->
            <rect width="150" height="80" fill="#a8d8f0"/>
            <!-- Main dome -->
            <ellipse cx="75" cy="52" rx="22" ry="28" fill="#f5f0e8"/>
            <!-- Finial -->
            <rect x="73" y="24" width="4" height="14" fill="#d0c0a0"/>
            <circle cx="75" cy="22" r="4" fill="#c8b090"/>
            <!-- Main building -->
            <rect x="35" y="72" width="80" height="55" fill="#f0ece0"/>
            <!-- Arched entrance -->
            <path d="M65,127 Q65,98 75,95 Q85,98 85,127" fill="#d8ccc0"/>
            <ellipse cx="75" cy="95" rx="12" ry="5" fill="#d8ccc0"/>
            <!-- Side minarets -->
            <rect x="22" y="90" width="10" height="55" fill="#f0ece0"/>
            <ellipse cx="27" cy="87" rx="6" ry="10" fill="#f0ece0"/>
            <circle cx="27" cy="79" r="3" fill="#c8b090"/>
            <rect x="118" y="90" width="10" height="55" fill="#f0ece0"/>
            <ellipse cx="123" cy="87" rx="6" ry="10" fill="#f0ece0"/>
            <circle cx="123" cy="79" r="3" fill="#c8b090"/>
            <!-- Reflecting pool -->
            <rect x="55" y="127" width="40" height="23" fill="#88c8e8" opacity=".7"/>
            <!-- Marble inlay pattern -->
            <g fill="none" stroke="#d0c0a0" stroke-width=".8" opacity=".5">
              <rect x="40" y="76" width="70" height="50"/>
              <line x1="75" y1="76" x2="75" y2="126"/>
            </g>
          </svg>
        </div>
        <div class="ig-04__meta">Taj Mahal, 2002</div>
      </div>
      <!-- Polaroid 3: Santorini sunset -->
      <div class="ig-04__card">
        <div class="ig-04__photo">
          <svg viewBox="0 0 150 150" xmlns="http://www.w3.org/2000/svg">
            <defs><linearGradient id="ssg" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#ff6b35"/><stop offset="40%" stop-color="#ff9850"/><stop offset="70%" stop-color="#ffcc80"/><stop offset="100%" stop-color="#2266cc"/></linearGradient></defs>
            <rect width="150" height="150" fill="url(#ssg)"/>
            <circle cx="75" cy="60" r="18" fill="#fff5a0" opacity=".9"/>
            <!-- Whitewashed buildings -->
            <g fill="#f5f5f0">
              <rect x="0" y="90" width="30" height="60"/><rect x="28" y="80" width="28" height="70"/>
              <rect x="54" y="88" width="25" height="62"/><rect x="78" y="75" width="30" height="75"/>
              <rect x="107" y="85" width="28" height="65"/><rect x="133" y="92" width="17" height="58"/>
            </g>
            <!-- Blue domes -->
            <g fill="#2266cc">
              <ellipse cx="42" cy="78" rx="14" ry="10"/><ellipse cx="93" cy="73" rx="16" ry="12"/>
            </g>
            <!-- Windows, blue detail -->
            <g fill="#4488ee">
              <rect x="5" y="100" width="8" height="10" rx="2"/><rect x="18" y="100" width="8" height="10" rx="2"/>
              <rect x="33" y="88" width="8" height="10" rx="2"/><rect x="83" y="84" width="8" height="10" rx="2"/>
            </g>
            <!-- Sea -->
            <rect x="0" y="140" width="150" height="10" fill="#1144aa" opacity=".6"/>
          </svg>
        </div>
        <div class="ig-04__meta">Santorini, 2005</div>
      </div>
      <!-- Polaroid 4: Safari elephant -->
      <div class="ig-04__card">
        <div class="ig-04__photo">
          <svg viewBox="0 0 150 150" xmlns="http://www.w3.org/2000/svg">
            <defs><linearGradient id="sfg" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#e8c870"/><stop offset="50%" stop-color="#d4a840"/><stop offset="100%" stop-color="#8a5010"/></linearGradient></defs>
            <rect width="150" height="150" fill="url(#sfg)"/>
            <!-- Acacia tree -->
            <rect x="110" y="80" width="6" height="70" fill="#4a2808"/>
            <ellipse cx="113" cy="78" rx="28" ry="12" fill="#2a5a14"/>
            <ellipse cx="100" cy="72" rx="20" ry="9" fill="#3a7020"/>
            <ellipse cx="126" cy="70" rx="18" ry="8" fill="#2a5a14"/>
            <!-- Elephant -->
            <g transform="translate(20,80)" fill="#7a7060">
              <ellipse cx="40" cy="30" rx="38" ry="26"/> <!-- body -->
              <ellipse cx="14" cy="22" rx="18" ry="20"/> <!-- head -->
              <!-- Ear -->
              <ellipse cx="2" cy="18" rx="12" ry="15" fill="#8a8070"/>
              <!-- Trunk -->
              <path d="M5,32 Q-5,42 -2,58 Q0,65 5,60 Q8,52 5,42 Q12,38 10,32" fill="#7a7060"/>
              <!-- Legs -->
              <rect x="10" y="50" width="12" height="30" rx="5" fill="#6a6050"/>
              <rect x="26" y="50" width="12" height="32" rx="5" fill="#6a6050"/>
              <rect x="44" y="50" width="12" height="30" rx="5" fill="#6a6050"/>
              <rect x="60" y="50" width="12" height="28" rx="5" fill="#6a6050"/>
              <!-- Tusk -->
              <path d="M8,36 Q-2,44 -5,52" stroke="#f0e8c0" stroke-width="3" fill="none" stroke-linecap="round"/>
              <!-- Eye -->
              <circle cx="8" cy="16" r="3" fill="#1a1a14"/>
              <circle cx="9" cy="15" r="1" fill="#fff"/>
              <!-- Tail -->
              <path d="M78,32 Q88,35 90,44" stroke="#6a6050" stroke-width="2.5" fill="none"/>
            </g>
            <!-- Baby elephant -->
            <g transform="translate(72,100)" fill="#8a8070" opacity=".85">
              <ellipse cx="18" cy="16" rx="18" ry="13"/>
              <ellipse cx="5" cy="11" rx="9" ry="10"/>
              <path d="M2,15 Q-4,22 -2,30 Q0,34 3,30 Q5,24 2,18" fill="#8a8070"/>
              <rect x="4" y="25" width="6" height="14" rx="3" fill="#7a7060"/>
              <rect x="13" y="25" width="6" height="14" rx="3" fill="#7a7060"/>
              <rect x="22" y="25" width="6" height="14" rx="3" fill="#7a7060"/>
              <rect x="30" y="25" width="6" height="12" rx="3" fill="#7a7060"/>
            </g>
            <!-- Grass tufts -->
            <g stroke="#6a8a28" stroke-width="1.5" stroke-linecap="round" fill="none" opacity=".7">
              <line x1="5" y1="148" x2="3" y2="135"/><line x1="10" y1="148" x2="12" y2="136"/>
              <line x1="145" y1="148" x2="143" y2="134"/><line x1="138" y1="148" x2="140" y2="136"/>
            </g>
          </svg>
        </div>
        <div class="ig-04__meta">Amboseli, 2010</div>
      </div>
      <!-- Polaroid 5: Northern Lights cabin -->
      <div class="ig-04__card">
        <div class="ig-04__photo">
          <svg viewBox="0 0 150 150" xmlns="http://www.w3.org/2000/svg">
            <defs><linearGradient id="nlg" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#040820"/><stop offset="50%" stop-color="#0a1535"/><stop offset="100%" stop-color="#0d1e25"/></linearGradient><filter id="nlf"><feGaussianBlur stdDeviation="4"/></filter></defs>
            <rect width="150" height="150" fill="url(#nlg)"/>
            <g opacity=".7" filter="url(#nlf)">
              <path d="M0,45 Q38,22 75,45 Q112,62 150,32" stroke="#00e8a0" stroke-width="18" fill="none"/>
              <path d="M0,62 Q40,38 80,56 Q118,75 150,48" stroke="#6040ff" stroke-width="12" fill="none"/>
              <path d="M0,35 Q30,18 60,38 Q95,58 150,26" stroke="#00c8e0" stroke-width="8" fill="none"/>
            </g>
            <g fill="#fff" opacity=".7"><circle cx="10" cy="8" r=".8"/><circle cx="30" cy="4" r="1"/><circle cx="60" cy="10" r=".9"/><circle cx="90" cy="5" r="1.1"/><circle cx="120" cy="8" r=".8"/><circle cx="145" cy="4" r="1"/></g>
            <!-- Snow ground -->
            <rect x="0" y="115" width="150" height="35" fill="#c8d8e8"/>
            <path d="M0,112 Q38,104 75,110 Q112,104 150,108 L150,115 L0,115 Z" fill="#d8e8f5"/>
            <!-- Cabin -->
            <g fill="#2a1808"><rect x="50" y="95" width="50" height="28"/><polygon points="44,95 75,78 106,95"/></g>
            <rect x="55" y="100" width="12" height="12" fill="#ff9040" opacity=".9"/>
            <rect x="83" y="100" width="12" height="12" fill="#ff9040" opacity=".9"/>
            <rect x="67" y="105" width="10" height="18" fill="#5a2808"/>
            <!-- Window glow -->
            <rect x="55" y="100" width="12" height="12" fill="#ffaa60" opacity=".4" filter="url(#nlf)"/>
            <rect x="83" y="100" width="12" height="12" fill="#ffaa60" opacity=".4" filter="url(#nlf)"/>
            <!-- Pine trees -->
            <g fill="#0d1e10">
              <polygon points="15,115 22,88 29,115"/><polygon points="18,100 22,88 26,100"/>
              <polygon points="125,115 132,92 139,115"/><polygon points="128,102 132,92 136,102"/>
            </g>
            <!-- Chimney smoke -->
            <path d="M76,78 Q74,68 78,58 Q80,48 76,40" stroke="#888" stroke-width="3" fill="none" opacity=".5" stroke-linecap="round"/>
          </svg>
        </div>
        <div class="ig-04__meta">Lapland, 2015</div>
      </div>
    </div>
  </div>
  <p class="ig-04__hint">Hover to fan · Click to flip</p>
</div>
<script>
(function(){
  const stack=document.getElementById('ig-04-stack');
  if(!stack)return;
  const cards=Array.from(stack.querySelectorAll('.ig-04__card'));
  const rots=cards.map(()=>(Math.random()-.5)*16);
  cards.forEach((c,i)=>{c.style.transform=`rotate(${rots[i]}deg)`;c.style.zIndex=i;});
  stack.addEventListener('mouseenter',()=>{
    const spread=20,total=cards.length;
    cards.forEach((c,i)=>{
      const angle=-spread/2+(spread/(total-1))*i;
      const tx=-130+(260/(total-1))*i;
      c.style.transform=`translateX(${tx}px) rotate(${angle}deg)`;
      c.style.setProperty('--ft',`translateX(${tx}px) rotate(${angle}deg)`);
    });
  });
  stack.addEventListener('mouseleave',()=>{
    cards.forEach((c,i)=>{c.style.transform=`rotate(${rots[i]}deg)`;c.classList.remove('ig-04--active');});
  });
  cards.forEach((c,i)=>{
    c.addEventListener('click',()=>{
      cards.forEach((x,j)=>{x.classList.remove('ig-04--active');x.style.zIndex=j;});
      c.classList.add('ig-04--active');c.style.zIndex=cards.length+1;
      c.addEventListener('animationend',()=>c.classList.remove('ig-04--active'),{once:true});
    });
  });
}());
</script>
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
body{background:radial-gradient(ellipse at center,#3a2a1a 0%,#1a1008 100%);font-family:'DM Sans',sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:2rem}
.ig-04{--card-w:170px;display:flex;flex-direction:column;align-items:center;gap:1.8rem}
.ig-04__scene{perspective:900px;height:260px;width:100%;display:flex;align-items:center;justify-content:center}
.ig-04__stack{position:relative;width:var(--card-w);height:220px;cursor:pointer}
.ig-04__card{position:absolute;inset:0;background:#faf6ee;border-radius:3px;padding:10px 10px 44px;box-shadow:0 8px 30px rgba(0,0,0,.55),0 2px 6px rgba(0,0,0,.3);transition:transform .5s cubic-bezier(.34,1.56,.64,1),box-shadow .4s ease;will-change:transform;transform-origin:center bottom}
.ig-04__photo{width:100%;aspect-ratio:1;display:block}
.ig-04__photo svg{width:100%;height:100%;display:block}
.ig-04__meta{text-align:center;font-family:'Caveat',cursive;font-size:1rem;color:#4a3a28;margin-top:.4rem;letter-spacing:.02em}
.ig-04__card.ig-04--active{animation:ig-04-flip .65s cubic-bezier(.34,1.56,.64,1);box-shadow:0 28px 70px rgba(0,0,0,.65)}
@keyframes ig-04-flip{0%{transform:var(--ft,rotate(0deg)) scale(1)}50%{transform:var(--ft,rotate(0deg)) scale(1.18) rotateY(180deg)}100%{transform:var(--ft,rotate(0deg)) scale(1.1) rotateY(360deg)}}
.ig-04__hint{color:rgba(255,255,255,.3);font-size:.72rem;letter-spacing:.1em;text-transform:uppercase}
@media(prefers-reduced-motion:reduce){.ig-04__card{transition:none}.ig-04__card.ig-04--active{animation:none}}
(function(){
  const stack=document.getElementById('ig-04-stack');
  if(!stack)return;
  const cards=Array.from(stack.querySelectorAll('.ig-04__card'));
  const rots=cards.map(()=>(Math.random()-.5)*16);
  cards.forEach((c,i)=>{c.style.transform=`rotate(${rots[i]}deg)`;c.style.zIndex=i;});
  stack.addEventListener('mouseenter',()=>{
    const spread=20,total=cards.length;
    cards.forEach((c,i)=>{
      const angle=-spread/2+(spread/(total-1))*i;
      const tx=-130+(260/(total-1))*i;
      c.style.transform=`translateX(${tx}px) rotate(${angle}deg)`;
      c.style.setProperty('--ft',`translateX(${tx}px) rotate(${angle}deg)`);
    });
  });
  stack.addEventListener('mouseleave',()=>{
    cards.forEach((c,i)=>{c.style.transform=`rotate(${rots[i]}deg)`;c.classList.remove('ig-04--active');});
  });
  cards.forEach((c,i)=>{
    c.addEventListener('click',()=>{
      cards.forEach((x,j)=>{x.classList.remove('ig-04--active');x.style.zIndex=j;});
      c.classList.add('ig-04--active');c.style.zIndex=cards.length+1;
      c.addEventListener('animationend',()=>c.classList.remove('ig-04--active'),{once:true});
    });
  });
}());

How this works

Each polaroid card has a random rotation applied via JavaScript on load using Math.random() to set an inline transform: rotate(Ndeg). On stack hover, JS fans the cards with both translateX and rotate values spread evenly across the count — creating a arc effect. A CSS transition: transform .5s cubic-bezier(.34,1.56,.64,1) provides the spring bounce.

The click flip uses a CSS @keyframes ig-04-flip animation that rotates the card 360 degrees in rotateY while briefly scaling up, applied by toggling the .ig-04--active class. The --ft CSS custom property stores the fanned position so the flip starts and ends at the correct angle.

Customize

  • Widen the fan spread by increasing the spread variable in the JS from 20 to 30 degrees, and the translateX range from 260 to 360px.
  • Change the polaroid padding-bottom to adjust the caption space — padding: 10px 10px 44px controls the white border and lower white strip.
  • Make all cards the same rotation on load by removing the Math.random() and setting a fixed stagger like i * 5 - 10 degrees.
  • Add a tape strip above each card by inserting a small ::before pseudo-element with background: rgba(255,240,200,.55).
  • Change font to a different handwritten feel by swapping Caveat for Pacifico or Satisfy in the Google Fonts import.

Watch out for

  • The --ft CSS custom property set inline via JS can be overwritten by later JS transforms; always set it before applying the active class.
  • animation-fill-mode: forwards is required on the flip keyframe, otherwise the card snaps back to its pre-animation state when the animation ends.
  • CSS perspective on the parent must be set for rotateY to look three-dimensional — without perspective: 900px on a parent, the flip appears flat.

Browser support

ChromeSafariFirefoxEdge
36+ 9+ 16+ 36+

CSS transforms and 3D perspective broadly supported; CSS custom properties require Chrome 49+.

Search CodeFronts

Loading…