Letters spin through random glyphs and lock into place, one by one. A small moment of mechanical drama for any heading that wants to feel like it just arrived instead of being there all along.
Reach for it when you have a hero or title that wants more presence, when the page is otherwise calm and you want one moment of mechanical drama, or when you want a click target that gives delight without a full animation.
Skip it when the text is long (more than ~30 chars feels noisy), when the page is already animation-heavy, or when readers need to read fast.
Add the class flip to any heading. The data-final attribute holds the text that will lock in.
<h1 class="flip" data-final="Sophie's Swamp">Sophie's Swamp</h1>
Two states: spinning (lime) and locked (cream). Swap the colors for your palette.
.flip { cursor: pointer; } .flip .ch { display: inline-block; min-width: .55em; text-align: center; transition: color .15s; } .flip.flipping .ch { color: #b6ff5b; /* spinning: neon lime */ text-shadow: 0 0 14px rgba(182,255,91,.55); } .flip.flipping .ch.locked { color: #f3eedd; /* locked: cream */ text-shadow: 0 0 20px rgba(82,255,224,.25); }
Drop into a <script> tag at the bottom of your page. Runs once on load. Click any .flip to replay.
// Swamp UI — Split-Flap Text (function () { const POOL = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#@%&*+=°/'; function rand() { return POOL[Math.floor(Math.random() * POOL.length)]; } function splitInto(el, finalText) { el.innerHTML = ''; const chars = [...finalText]; return chars.map(ch => { const s = document.createElement('span'); s.className = 'ch'; s.dataset.final = ch; s.textContent = (ch === ' ') ? ' ' : rand(); el.appendChild(s); return s; }); } function play(el) { const finalText = el.dataset.final || el.textContent.trim(); const spans = splitInto(el, finalText); el.classList.add('flipping'); const STEP = 55; // ms between random swaps const PER_LETTER = 9; // # of swaps before locking const STAGGER = 70; // ms offset per letter spans.forEach((s, i) => { const target = s.dataset.final; if (target === ' ') { s.classList.add('locked'); return; } let swaps = 0; setTimeout(function spin() { if (swaps >= PER_LETTER) { s.textContent = target; s.classList.add('locked'); if (spans.every(x => x.classList.contains('locked'))) { setTimeout(() => el.classList.remove('flipping'), 400); } return; } s.textContent = rand(); swaps++; setTimeout(spin, STEP); }, i * STAGGER); }); } // Run on font-load (so glyph widths are right) function runAll() { document.querySelectorAll('.flip').forEach(el => play(el)); } if (document.fonts && document.fonts.ready) { document.fonts.ready.then(() => setTimeout(runAll, 250)); } else { setTimeout(runAll, 600); } // Click to replay document.addEventListener('click', e => { const el = e.target.closest('.flip'); if (el && !el.classList.contains('flipping')) play(el); }); })();
| Constant | Default | Effect |
|---|---|---|
POOL |
A–Z 0–9 #@%&*+=°/ | Which random glyphs flash before locking. Add emoji for chaos. |
STEP |
55 ms | How fast each letter cycles. Lower = faster spin. |
PER_LETTER |
9 | Random glyphs shown per letter before locking. Higher = longer animation. |
STAGGER |
70 ms | Delay between letters starting. Higher = wavier reveal. |
Total duration ≈ (N_chars × STAGGER) + (PER_LETTER × STEP) + 400.
For 14 chars: ~1900 ms.
Same logic, different palette. Click any to play.