Skip to content
animorio
IT
Back to the library

The postcard turn

Transitions, SVGGSAP, CSS onlyAdvancedCompressed weight: Demo 16 KBGSAP 28 KBFreeDownload the zip

The page is the front of a picture postcard lying on a table: a click lifts it, turns it over in 3D, and on the back a pen writes the message and the address, a stamp flies in and a round rubber stamp cancels it. Then it turns again and shows the postcard of another place.

The code

A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN. The React and Vue tabs hold the same demo as a ready component, which switches its animations off when it leaves the page.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
  <title>The postcard turn</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/full.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/caveat@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage" data-stage>
    <div class="desk" aria-hidden="true"></div>

    <!-- the shadow on the table: a tight one when the card lies flat, a soft one when it lifts -->
    <div class="shadow" data-shadow aria-hidden="true">
      <div class="shadow__soft" data-shadow-soft></div>
      <div class="shadow__tight" data-shadow-tight></div>
    </div>

    <!-- the postcard: the hover lean (tilt), the lift and the turn (card), then the two faces and the thick paper -->
    <div class="tilt" data-tilt>
      <div class="card" data-card role="img" aria-label="A picture postcard">

        <!-- the front: a picture with a white border -->
        <div class="face face--front" data-front data-place="0">
          <div class="paper paper--front">
            <div class="print">

              <!-- Port Halloran: a lighthouse on the cliff, a sailing boat, the beach -->
              <svg class="scene" data-scene="0" data-tall-x="128" viewBox="0 0 600 400" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
                <defs>
                  <linearGradient id="sky0" x1="0" y1="0" x2="0" y2="1">
                    <stop offset="0" stop-color="#f4b98a"/>
                    <stop offset="0.6" stop-color="#fbe6c8"/>
                  </linearGradient>
                  <linearGradient id="sea0" x1="0" y1="0" x2="0" y2="1">
                    <stop offset="0" stop-color="#4f9bb1"/>
                    <stop offset="1" stop-color="#235670"/>
                  </linearGradient>
                  <linearGradient id="beam0" x1="0" y1="0" x2="1" y2="0">
                    <stop offset="0" stop-color="#fff3c6" stop-opacity="0.9"/>
                    <stop offset="1" stop-color="#fff3c6" stop-opacity="0"/>
                  </linearGradient>
                  <clipPath id="tower0"><polygon points="164,162 188,162 184,92 168,92"/></clipPath>
                </defs>
                <g id="art-0">
                  <rect width="600" height="400" fill="url(#sky0)"/>
                  <g class="i-sun" data-o="395 150">
                    <circle class="i-rays" cx="395" cy="150" r="66" fill="none" stroke="#f9c98a" stroke-width="34" stroke-dasharray="11 23.56" opacity="0.55"/>
                    <circle cx="395" cy="150" r="44" fill="#f39b45"/>
                  </g>
                  <g class="i-cloud" fill="#fff4e4">
                    <ellipse cx="128" cy="78" rx="34" ry="13"/><ellipse cx="148" cy="68" rx="20" ry="14"/><ellipse cx="114" cy="72" rx="14" ry="10"/>
                  </g>
                  <g class="i-cloud i-cloud--b" fill="#fff4e4" opacity="0.85">
                    <ellipse cx="500" cy="62" rx="40" ry="11"/><ellipse cx="515" cy="54" rx="20" ry="11"/>
                  </g>
                  <path d="M330 236 C380 222 420 214 470 216 C520 218 560 226 600 230 V238 H330Z" fill="#9aa07a"/>
                  <rect y="236" width="600" height="164" fill="url(#sea0)"/>
                  <polygon class="i-beam" points="182,78 420,50 420,108" fill="url(#beam0)" opacity="0.55"/>
                  <g class="i-wave"><path class="wave" data-y="252" data-a="2.5" data-l="40" fill="none" stroke="#a6dbe2" stroke-width="2" opacity="0.6"/></g>
                  <g class="i-wave i-wave--b"><path class="wave" data-y="276" data-a="3" data-l="50" fill="none" stroke="#a6dbe2" stroke-width="2.4" opacity="0.5"/></g>
                  <g class="i-boat" data-o="380 272">
                    <ellipse cx="381" cy="282" rx="32" ry="4" fill="#1d4a60" opacity="0.35"/>
                    <line x1="381" y1="268" x2="381" y2="212" stroke="#4a3426" stroke-width="2"/>
                    <path d="M383 264 L383 214 L408 264Z" fill="#fff8ec"/>
                    <path d="M379 264 L379 222 L358 264Z" fill="#f3dfc0"/>
                    <path d="M352 266 H410 L402 278 H360Z" fill="#c8442f"/>
                    <path d="M356 270 H406" stroke="#fff3e2" stroke-width="1.6"/>
                  </g>
                  <g class="i-wave i-wave--c"><path class="wave" data-y="304" data-a="4" data-l="60" fill="none" stroke="#bfe6ea" stroke-width="2.8" opacity="0.55"/></g>
                  <path d="M0 150 C60 146 150 150 230 176 C262 186 278 214 286 250 C292 290 300 340 320 400 H0Z" fill="#6f8a45"/>
                  <path d="M230 176 C262 186 278 214 286 250 C292 290 300 340 320 400 H262 C250 340 246 290 236 250 C228 216 214 196 196 182Z" fill="#b06b4b"/>
                  <path d="M244 214 C254 230 258 252 262 270 M256 300 C262 330 270 356 280 384" fill="none" stroke="#8e523a" stroke-width="3" stroke-linecap="round"/>
                  <path d="M0 154 C60 150 150 154 228 179" fill="none" stroke="#93b35f" stroke-width="5" stroke-linecap="round"/>
                  <path d="M0 210 C70 200 140 220 200 260 C230 300 236 350 250 400 H0Z" fill="#5e7a3a"/>
                  <rect x="190" y="146" width="24" height="16" fill="#f6eddc"/>
                  <polygon points="187,147 217,147 202,136" fill="#c8442f"/>
                  <rect x="199" y="152" width="6" height="10" fill="#4a3426"/>
                  <polygon points="164,162 188,162 184,92 168,92" fill="#f8f1e4"/>
                  <g clip-path="url(#tower0)" fill="#c8442f">
                    <rect x="150" y="138" width="50" height="11"/>
                    <rect x="150" y="113" width="50" height="11"/>
                  </g>
                  <rect x="163" y="86" width="26" height="6" rx="1" fill="#3b3a38"/>
                  <rect class="i-lamp" x="169" y="72" width="14" height="14" fill="#ffe08a"/>
                  <path d="M169 72 V86 M176 72 V86 M183 72 V86" stroke="#3b3a38" stroke-width="1.4"/>
                  <polygon points="166,72 186,72 176,60" fill="#c8442f"/>
                  <line x1="176" y1="60" x2="176" y2="54" stroke="#3b3a38" stroke-width="1.6"/>
                  <path d="M330 400 C380 352 470 330 600 322 V400Z" fill="#f1d39b"/>
                  <path d="M380 400 C420 370 500 352 600 348 V400Z" fill="#e7c385"/>
                  <g class="i-bird" data-o="250 96"><path d="M240 96 q5 -6 10 0 q5 -6 10 0" fill="none" stroke="#4a3a34" stroke-width="2" stroke-linecap="round"/></g>
                  <g class="i-bird i-bird--b" data-o="290 118"><path d="M282 118 q4 -5 8 0 q4 -5 8 0" fill="none" stroke="#4a3a34" stroke-width="1.8" stroke-linecap="round"/></g>
                </g>
              </svg>

              <!-- Valdessa Lakes: snowy peaks, a lake, a red cabin and a rowing boat -->
              <svg class="scene" data-scene="1" viewBox="0 0 600 400" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
                <defs>
                  <linearGradient id="sky1" x1="0" y1="0" x2="0" y2="1">
                    <stop offset="0" stop-color="#86bcd3"/>
                    <stop offset="0.65" stop-color="#e6efe2"/>
                  </linearGradient>
                  <linearGradient id="lake1" x1="0" y1="0" x2="0" y2="1">
                    <stop offset="0" stop-color="#5e98ad"/>
                    <stop offset="1" stop-color="#2f6278"/>
                  </linearGradient>
                  <clipPath id="lakeclip1"><rect y="252" width="600" height="86"/></clipPath>
                </defs>
                <g id="art-1">
                  <rect width="600" height="400" fill="url(#sky1)"/>
                  <g class="i-sun" data-o="392 92">
                    <circle cx="392" cy="92" r="40" fill="#fff6d8" opacity="0.5"/>
                    <circle cx="392" cy="92" r="24" fill="#fff1c4"/>
                  </g>
                  <g class="i-cloud" fill="#ffffff" opacity="0.9">
                    <ellipse cx="160" cy="70" rx="38" ry="11"/><ellipse cx="178" cy="62" rx="20" ry="12"/>
                  </g>
                  <g class="i-cloud i-cloud--b" fill="#ffffff" opacity="0.8">
                    <ellipse cx="470" cy="128" rx="30" ry="8"/><ellipse cx="482" cy="122" rx="15" ry="8"/>
                  </g>
                  <path d="M0 200 L70 140 L120 170 L190 96 L250 160 L300 120 L360 176 L430 88 L500 150 L560 118 L600 140 V252 H0Z" fill="#94acc0"/>
                  <path d="M190 96 L174 112 L184 110 L191 118 L198 109 L208 114Z M430 88 L412 106 L423 104 L431 112 L439 103 L450 107Z M300 120 L288 132 L296 131 L302 137 L310 132Z" fill="#f4f1ea"/>
                  <path id="near1" d="M0 236 L80 170 L140 214 L230 150 L320 230 L400 168 L470 220 L540 180 L600 210 V254 H0Z" fill="#5d7590"/>
                  <path d="M230 150 L212 168 L224 165 L232 174 L242 164 L252 170Z M400 168 L384 182 L394 181 L402 188 L410 180 L420 184Z" fill="#eef0ec"/>
                  <path class="pines" data-y="254" data-h="14" data-s="9" fill="#3a6448"/>
                  <rect y="252" width="600" height="86" fill="url(#lake1)"/>
                  <g clip-path="url(#lakeclip1)" opacity="0.28">
                    <use href="#near1" transform="translate(0 506) scale(1 -1)"/>
                  </g>
                  <g class="i-wave"><path class="wave" data-y="270" data-a="1.6" data-l="34" fill="none" stroke="#d6eef0" stroke-width="1.6" opacity="0.55"/></g>
                  <g class="i-wave i-wave--b"><path class="wave" data-y="300" data-a="2" data-l="44" fill="none" stroke="#d6eef0" stroke-width="1.8" opacity="0.45"/></g>
                  <rect x="182" y="232" width="30" height="18" fill="#b4432f"/>
                  <polygon points="178,233 216,233 197,218" fill="#4a2b22"/>
                  <rect x="204" y="216" width="5" height="10" fill="#4a2b22"/>
                  <rect x="193" y="240" width="7" height="10" fill="#4a2b22"/>
                  <rect x="185" y="237" width="5" height="5" fill="#ffe7a6"/>
                  <g class="i-smoke" fill="#ffffff" opacity="0.7">
                    <circle cx="207" cy="210" r="4"/><circle cx="211" cy="202" r="5"/><circle cx="217" cy="193" r="6"/>
                  </g>
                  <g class="i-boat" data-o="336 294">
                    <path d="M318 292 H354 L348 300 H324Z" fill="#7a4a2c"/>
                    <circle cx="336" cy="284" r="4" fill="#c8442f"/>
                    <rect x="333" y="287" width="6" height="6" fill="#c8442f"/>
                    <path d="M326 292 L312 304 M346 292 L360 304" stroke="#4a2b22" stroke-width="1.6" stroke-linecap="round"/>
                  </g>
                  <path d="M0 330 C120 318 220 334 320 326 C420 318 520 330 600 324 V400 H0Z" fill="#7da35a"/>
                  <path d="M0 356 C140 344 260 360 380 352 C480 346 560 354 600 350 V400 H0Z" fill="#6a9149"/>
                  <g fill="#f4ead0"><circle cx="240" cy="352" r="2.2"/><circle cx="262" cy="362" r="2"/><circle cx="300" cy="348" r="2.4"/><circle cx="350" cy="366" r="2"/><circle cx="390" cy="356" r="2.2"/><circle cx="214" cy="372" r="2"/></g>
                  <g fill="#e2896a"><circle cx="252" cy="372" r="2"/><circle cx="326" cy="360" r="2.2"/><circle cx="372" cy="380" r="2"/><circle cx="286" cy="384" r="2.2"/></g>
                  <path d="M60 400 L60 380 L30 380 L58 340 L38 340 L62 300 L46 300 L72 254 L98 300 L82 300 L106 340 L86 340 L114 380 L84 380 L84 400Z" fill="#2c5a3c"/>
                  <path d="M520 400 L520 384 L496 384 L518 350 L502 350 L524 314 L510 314 L532 274 L554 314 L540 314 L562 350 L546 350 L568 384 L544 384 L544 400Z" fill="#2c5a3c"/>
                  <g class="i-bird" data-o="282 124"><path d="M274 124 q4 -5 8 0 q4 -5 8 0" fill="none" stroke="#3d4a52" stroke-width="1.8" stroke-linecap="round"/></g>
                  <g class="i-bird i-bird--b" data-o="306 110"><path d="M300 110 q3 -4 6 0 q3 -4 6 0" fill="none" stroke="#3d4a52" stroke-width="1.6" stroke-linecap="round"/></g>
                </g>
              </svg>

              <!-- Saint Amaury: a hill town with a bell tower, cypresses, vineyards and a balloon -->
              <svg class="scene" data-scene="2" viewBox="0 0 600 400" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
                <defs>
                  <linearGradient id="sky2" x1="0" y1="0" x2="0" y2="1">
                    <stop offset="0" stop-color="#eea866"/>
                    <stop offset="0.7" stop-color="#f9e2bb"/>
                  </linearGradient>
                </defs>
                <g id="art-2">
                  <rect width="600" height="400" fill="url(#sky2)"/>
                  <g class="i-sun" data-o="300 132">
                    <circle class="i-rays" cx="300" cy="132" r="80" fill="none" stroke="#fbd79b" stroke-width="40" stroke-dasharray="13 28.9" opacity="0.5"/>
                    <circle cx="300" cy="132" r="56" fill="#fde3a6"/>
                  </g>
                  <g class="i-cloud" fill="#fff4e2" opacity="0.9">
                    <ellipse cx="120" cy="92" rx="36" ry="10"/><ellipse cx="136" cy="84" rx="18" ry="10"/>
                  </g>
                  <path d="M0 230 C90 196 170 212 250 200 C350 186 450 206 600 192 V320 H0Z" fill="#d9ae63"/>
                  <path d="M0 262 C80 240 160 250 220 244 L380 244 C460 250 540 236 600 246 V330 H0Z" fill="#c39a50"/>
                  <path d="M40 400 C110 300 200 214 300 200 C400 214 490 300 560 400Z" fill="#9aa64f"/>
                  <path d="M300 214 C270 250 340 270 310 310 C286 340 320 370 330 400" fill="none" stroke="#ecd8a8" stroke-width="7" stroke-linecap="round"/>
                  <rect x="290" y="128" width="20" height="76" fill="#efd9b4"/>
                  <path d="M296 142 a4 4 0 0 1 8 0 v8 h-8z" fill="#4a3426"/>
                  <polygon points="287,128 313,128 300,108" fill="#c4593a"/>
                  <rect x="286" y="126" width="28" height="4" fill="#d8bf95"/>
                  <g fill="#f1dfbf">
                    <rect x="236" y="190" width="28" height="22"/><rect x="262" y="182" width="26" height="30"/>
                    <rect x="312" y="186" width="28" height="26"/><rect x="338" y="194" width="26" height="18"/>
                    <rect x="214" y="200" width="24" height="16"/><rect x="362" y="202" width="24" height="14"/>
                  </g>
                  <g fill="#c4593a">
                    <polygon points="233,191 267,191 250,181"/><polygon points="259,183 291,183 275,172"/>
                    <polygon points="309,187 343,187 326,176"/><polygon points="335,195 367,195 351,186"/>
                    <polygon points="211,201 241,201 226,193"/><polygon points="359,203 389,203 374,195"/>
                  </g>
                  <g fill="#6a4b36">
                    <rect x="244" y="198" width="4" height="6"/><rect x="254" y="198" width="4" height="6"/><rect x="270" y="190" width="4" height="6"/>
                    <rect x="279" y="200" width="4" height="6"/><rect x="320" y="194" width="4" height="6"/><rect x="330" y="200" width="4" height="6"/>
                    <rect x="348" y="200" width="4" height="5"/><rect x="222" y="206" width="4" height="5"/><rect x="372" y="207" width="4" height="4"/>
                  </g>
                  <g fill="#3c5b3a">
                    <ellipse cx="206" cy="226" rx="6" ry="24"/><ellipse cx="396" cy="232" rx="6" ry="26"/>
                    <ellipse cx="262" cy="292" rx="9" ry="34"/><ellipse cx="366" cy="318" rx="10" ry="38"/><ellipse cx="430" cy="286" rx="8" ry="30"/>
                  </g>
                  <path d="M0 352 C150 330 450 330 600 352 V400 H0Z" fill="#b5bd62"/>
                  <path d="M0 360 C150 340 450 340 600 360 M0 374 C150 354 450 354 600 374 M0 388 C150 368 450 368 600 388" fill="none" stroke="#728b3d" stroke-width="3.4" stroke-dasharray="6 5"/>
                  <g class="i-balloon" data-o="404 104">
                    <path d="M404 64 C428 64 434 90 422 108 L412 120 H396 L386 108 C374 90 380 64 404 64Z" fill="#c8442f"/>
                    <path d="M404 64 C412 64 416 90 410 108 L407 120 H401 L398 108 C392 90 396 64 404 64Z" fill="#fbecd2"/>
                    <path d="M397 120 L399 130 M411 120 L409 130" stroke="#5a3e2c" stroke-width="1.2"/>
                    <rect x="398" y="130" width="12" height="9" fill="#8a5a36"/>
                  </g>
                  <g class="i-bird" data-o="196 120"><path d="M188 120 q4 -5 8 0 q4 -5 8 0" fill="none" stroke="#5a3e34" stroke-width="1.8" stroke-linecap="round"/></g>
                  <g class="i-bird i-bird--b" data-o="214 104"><path d="M208 104 q3 -4 6 0 q3 -4 6 0" fill="none" stroke="#5a3e34" stroke-width="1.6" stroke-linecap="round"/></g>
                </g>
              </svg>

              <h1 class="title" data-title>
                <span class="title__hello" data-hello>Greetings from</span>
                <span class="title__name" data-name>Port Halloran</span>
              </h1>
            </div>
          </div>
        </div>

        <!-- the thickness of the card: a few layers of board between the faces -->
        <div class="slice slice--a" aria-hidden="true"></div>
        <div class="slice slice--b" aria-hidden="true"></div>
        <div class="slice slice--c" aria-hidden="true"></div>

        <!-- the back: the message, the address, the stamp and the postmark -->
        <div class="face face--back" data-back>
          <div class="paper paper--back">
            <div class="back">
              <p class="back__head">Post Card</p>
              <p class="back__caption" data-caption>The lighthouse at Gull Point</p>
              <div class="back__msg" data-msg></div>
              <div class="back__rule" aria-hidden="true"><span data-divider></span></div>
              <div class="back__box" data-box><span>Place stamp here</span></div>
              <div class="back__addr">
                <div class="back__lines" data-addr></div>
              </div>
              <p class="back__print">Halloran &amp; Fenn, Fine View Series</p>
            </div>
          </div>
          <div class="over" aria-hidden="true">
            <div class="stamp" data-stamp>
              <div class="stamp__lift" data-stamp-lift>
                <div class="stamp__paper">
                  <svg class="stamp__art" viewBox="110 40 200 250" preserveAspectRatio="xMidYMid slice" data-stamp-art><use href="#art-0" data-stamp-use/></svg>
                  <span class="stamp__value" data-stamp-value>4d</span>
                  <span class="stamp__label">Postage</span>
                </div>
              </div>
            </div>
            <div class="postmark" data-postmark>
              <svg viewBox="-60 -60 200 120">
                <defs>
                  <path id="pm-arc" d="M-32 0 a32 32 0 1 1 64 0"/>
                  <filter id="ink" x="-10%" y="-20%" width="120%" height="140%">
                    <feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="4" result="n"/>
                    <feDisplacementMap in="SourceGraphic" in2="n" scale="2.4" result="d"/>
                    <feTurbulence type="fractalNoise" baseFrequency="0.35" numOctaves="1" seed="9" result="m"/>
                    <feColorMatrix in="m" type="matrix" values="0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 -2.2 2.1" result="ma"/>
                    <feComposite in="d" in2="ma" operator="in"/>
                  </filter>
                </defs>
                <g filter="url(#ink)" class="postmark__ink">
                  <circle r="46" fill="none" stroke-width="3"/>
                  <circle r="25" fill="none" stroke-width="1.6"/>
                  <text class="postmark__ring"><textPath href="#pm-arc" startOffset="50%" text-anchor="middle" data-pm-place>PORT HALLORAN</textPath></text>
                  <text class="postmark__day" y="0" text-anchor="middle" data-pm-day>11 OCT</text>
                  <text class="postmark__year" y="13" text-anchor="middle">1962</text>
                  <path d="M52 -24 q12 -6 24 0 t24 0 t24 0 M54 -8 q12 -6 24 0 t24 0 t24 0 M54 8 q12 -6 24 0 t24 0 t24 0 M52 24 q12 -6 24 0 t24 0 t24 0" fill="none" stroke-width="2.4"/>
                </g>
              </svg>
            </div>
            <div class="tool" data-tool>
              <div class="tool__shadow" data-tool-shadow></div>
              <div class="tool__body" data-tool-body>
                <svg viewBox="-50 -50 100 100">
                  <defs>
                    <radialGradient id="knob" cx="0.36" cy="0.32" r="0.75">
                      <stop offset="0" stop-color="#e8b27a"/>
                      <stop offset="0.55" stop-color="#a8642f"/>
                      <stop offset="1" stop-color="#5e3215"/>
                    </radialGradient>
                  </defs>
                  <circle r="46" fill="#2f2622"/>
                  <circle r="42" fill="#4b3a33"/>
                  <circle r="30" fill="url(#knob)"/>
                  <path d="M-18 -16 a24 24 0 0 1 22 -10" fill="none" stroke="#fff1dc" stroke-width="3" stroke-linecap="round" opacity="0.7"/>
                </svg>
              </div>
            </div>
            <div class="pen" data-pen>
              <svg viewBox="0 0 60 160">
                <path d="M2 2 L12 30 L22 30 Z" fill="#c9b07a"/>
                <path d="M2 2 L12 30" stroke="#5b4a2a" stroke-width="1"/>
                <path d="M10 30 L24 30 L58 150 L44 158 Z" fill="#1f3b52"/>
                <path d="M14 30 L22 30 L50 128 L44 132 Z" fill="#3c6382" opacity="0.7"/>
                <path d="M24 36 L30 34 L46 92" stroke="#d8b878" stroke-width="2" fill="none" stroke-linecap="round"/>
              </svg>
            </div>
          </div>
        </div>
      </div>
    </div>

    <button class="act" type="button" data-act data-autoclic>
      <span class="act__fill" aria-hidden="true"></span>
      <span class="act__label" data-act-label>Turn it over</span>
    </button>

    <p class="sr-only" aria-live="polite" data-live></p>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script src="script.js"></script>
</body>
</html>

Playground

Change values, colours and text with the controls and watch the animation respond live. Or write in the code yourself. Then download the folder with your changes.

31 values to change

Or ask Claude Code for it

With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:

Take “The postcard turn” from Animorio and fit it into my site

What it does

On a dark green linen table a postcard with deckle edges comes in from below, tilted, and settles with a little bounce; on it is a drawn landscape, Port Halloran, with a lighthouse on the cliff, a sailing boat, the sun and gulls, and the title «Greetings from Port Halloran» in big raised letters that jump in one by one. The picture lives gently: the waves, the rocking boat, the beam of the lighthouse, the clouds. On a click the card presses a little into the table, then comes away and leans, its shadow spreading and blurring; it turns over in 3D and, while it is edge-on, you see the thickness of the board and the shadow becomes a thin line. On the back, printed «Post Card», the line down the middle and the address lines draw themselves; a pen comes in and writes the message by hand, line by line, then the address. A stamp with a crop of the picture flies in, spinning, and lands in its box with a bounce; a round wooden rubber stamp comes down from above, strikes (the card jolts) and lifts away, leaving the postmark with the name of the place and the date, in patchy ink. The card turns again and its front is now another place, Valdessa Lakes or Saint Amaury: it drops back on the table with a bounce and the new title jumps in. The next click goes on to the next place.

How it works

A card with two faces and a thickness

The card is an element with transform-style: preserve-3d holding two faces with backface-visibility: hidden: the front moved forward by half the thickness, the back turned by 180 degrees and moved back. In between sit three board-coloured sheets, a few tenths of a pixel apart: you only see them when the card is edge-on, and that is what makes it feel like thick paper. The stage has perspective, so the card that lifts (z) really comes closer.

Deckle edges with a mask

The teeth are a two-layer CSS mask: a rectangle one tooth smaller than the card, and a grid of circles one tooth wide repeated with round, so the circles of the outer row become the teeth and the size fits by itself. The same mask, finer, gives the stamp its perforations. The mask would cut the shadow too, so the shadow is a separate element on the table.

One object for card and shadow

Height, turn, lean and rotation live in one object (st) that GSAP animates; on every update a function writes the transform of the card and of its shadow. The shadow moves and spreads as the card rises, goes from the sharp one to the blurred one, and narrows with the cosine of the turn: edge-on it is a line. The card lifts with back.out, turns with power3.inOut and drops back with a little knock and elastic.out.

The writing pen

Every line of the message and the address is Caveat text uncovered by a clip-path inset that opens from left to right at a steady speed (letters per second). The tip of the pen follows the edge being uncovered: its place is measured with offsetLeft and offsetTop, which ignore the 3D transforms, and a sine makes it hop letter by letter. Between lines the pen moves to the start of the next one.

Stamp and round postmark

The stamp is a crop of the same picture (a use of the drawing with another viewBox), so it moves too. It comes in from outside, turned and enlarged, lands and squashes with an elastic. The wooden rubber stamp comes down getting smaller (it nears the paper) while its shadow tightens; at the moment of the strike the postmark appears, an SVG with text on an arc and the cancelling waves, roughened by a turbulence filter, and the card jolts. While the back is showing the front is swapped, so the second turn shows the new place.

Values to change

NameFileDefaultWhat it changes
LIFTscript.js1How high the card lifts off the table while it turns (0 = it turns lying flat).
TURNscript.js1.05The seconds of each turn of the card.
WRITEscript.js38How many letters a second the pen writes.
THUMPscript.js1How hard the round postmark strikes: how much it squashes and how much the card jolts.
TILTscript.js6How many degrees the card leans towards the pointer (mouse only).
TEETHscript.js1The size of the teeth along the edge of the card.
SPEEDscript.js1The speed of the whole transition: 2 is twice as fast, 0.5 half as fast.

Try them in the Playground

When to use it

To move from one place to another on a travel site, a hotel, a tourist office or a stationery shop; for a gallery of destinations, an invitation, a digital greeting card, a «write to us» page with a message that writes itself.

On phones

The same transition with the sizes adapted: on a tall screen the card stands upright, the back puts the message above and the address below, and every picture shows its own crop (the lighthouse stays in view). A tap on the card or the button turns it; the lean towards the pointer is for the mouse only.

With reduced motion

Nothing turns or moves: the card fades in; on a click the back, already written, with the stamp and the postmark, fades in over the front, stays a few seconds and fades away onto the front of the new place. The pictures stay still.

Pitfalls

  • The tooth mask also cuts the box-shadow and filter of the same element: the shadow is a separate element on the table, following the card (it moves, spreads and narrows with the turn).
  • A filter on the 3D container flattens preserve-3d and both faces show at once: no filters on the card or its parents, only on the flat pieces inside the faces (stamp, pen).
  • The mask of a face also cut the stamp flying in from outside: mask and writing live in an inner sheet, stamp, rubber stamp and pen in a layer above, with no mask.
  • getBoundingClientRect on the turned back gives sizes bent by the perspective: the place of the lines for the pen is summed from offsetLeft and offsetTop, which ignore transforms.
  • A fromTo sets its starting state at once: the stamp «in flight» showed on the back already during the first turn. The entrances of stamp, rubber stamp and pen use immediateRender: false.
  • The endless animations in the pictures all last 0.5, 2 or 4 seconds: so the scene comes back the same every 4 seconds and the preview video can tell where it ends.

Libraries

GSAP 3.15.0, Fraunces (font variabile) 5.3.0, Caveat (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 16 KB; the libraries: GSAP 28 KB.

Testing

Tested on 11 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: To do

License

Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.

All animations