Liveloop

An interactive timeline for social media. Every post is a tiny app you can play, save, and remix.

Product

  • Feed
  • Create
  • Claude Code plugin
  • Blog

Legal

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA

Project

  • Templates
© 2026 Liveloop. All rights reserved.
LiveloopVersion history

v1Current

@liveloop · 5/15/2026, 2:13:08 PM

Initial version — all lines are new.

+<style>
+ html, body { background: #fef9c3; color: #111; font-family: system-ui, sans-serif; }
+ .wrap { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; gap: 0.75rem; padding: 1rem; }
+ h1 { margin: 0; font-size: 0.875rem; letter-spacing: 0.15em; text-transform: uppercase; color: #71717a; font-weight: 600; }
+ .grid { display: grid; gap: 0.5rem; }
+ /* Cards are real <button>s (reliable tap target) with the native
+ button chrome reset away. A face-down card is genuinely empty —
+ the emoji text is only added when flipped. The old approach hid
+ the emoji with color:transparent and revealed it by switching the
+ color back, which did not repaint emoji glyphs on mobile Safari
+ (flipped cards showed blank white). */
+ .card { appearance: none; -webkit-appearance: none; border: 0; margin: 0; padding: 0; font-family: inherit; color: inherit; aspect-ratio: 1; display: flex; align-items: center; justify-content: center; border-radius: 0.5rem; background: #fde047; font-size: 1.5rem; line-height: 1; cursor: pointer; user-select: none; -webkit-user-select: none; touch-action: manipulation; transition: background-color 0.15s; }
+ .card.flipped { background: #fff; }
+ .card .face-img { width: 82%; height: 82%; object-fit: cover; border-radius: 0.35rem; }
+ .card.matched { background: #86efac; cursor: default; }
+ .status { font-size: 0.875rem; color: #52525b; }
+ .again { padding: 0.5rem 1rem; border-radius: 9999px; border: 0; background: #18181b; color: white; font-weight: 500; cursor: pointer; display: none; }
+ .again.visible { display: inline-block; }
+</style>
+<div class="wrap">
+ <h1>Memory: emojis</h1>
+ <div class="grid" id="grid"></div>
+ <div class="status" id="status">Tap a card to begin</div>
+ <button class="again" id="again">Play again</button>
+</div>
+<script>
+(function(){
+ var pairs = ["🌟","🌈","🍕","🎲","🦊","🚀"];
+ function isImg(v){ return typeof v === 'string' && v.indexOf('https://') === 0; }
+ function setFace(card, v){
+ if (isImg(v)) {
+ var img = document.createElement('img');
+ img.src = v; img.alt = ''; img.className = 'face-img';
+ card.innerHTML = ''; card.appendChild(img);
+ } else { card.textContent = v; }
+ }
+ function clearFace(card){ card.innerHTML = ''; }
+ var grid = document.getElementById('grid');
+ var statusEl = document.getElementById('status');
+ var againBtn = document.getElementById('again');
+ var deck, flipped, matched, moves;
+
+ // Tap handling via pointer events instead of 'click'. Inside the
+ // scroll-snap feed on mobile, the browser frequently classifies a
+ // touch on a small target as a scroll and never synthesizes the
+ // 'click' — so cards looked dead. pointerdown/up fire regardless;
+ // we just confirm the finger barely moved (a tap, not a drag) and
+ // bail on pointercancel (the browser took the gesture for scroll).
+ function onTap(el, handler) {
+ var sx = 0, sy = 0, armed = false;
+ el.addEventListener('pointerdown', function(e){
+ armed = true; sx = e.clientX; sy = e.clientY;
+ });
+ el.addEventListener('pointercancel', function(){ armed = false; });
+ el.addEventListener('pointerup', function(e){
+ if (!armed) return;
+ armed = false;
+ if (Math.abs(e.clientX - sx) < 14 && Math.abs(e.clientY - sy) < 14) {
+ handler();
+ }
+ });
+ }
+
+ function setup() {
+ deck = pairs.concat(pairs).sort(function(){ return Math.random() - 0.5; });
+ flipped = [];
+ matched = 0;
+ moves = 0;
+ var cols = pairs.length <= 4 ? 4 : 4;
+ grid.style.gridTemplateColumns = 'repeat(' + cols + ', minmax(0, 1fr))';
+ grid.style.width = 'min(70vmin, 320px)';
+ grid.innerHTML = '';
+ deck.forEach(function(emoji, i){
+ var card = document.createElement('button');
+ card.type = 'button';
+ card.className = 'card';
+ card.dataset.idx = i;
+ card.dataset.emoji = emoji;
+ // Face-down: empty. The emoji is added on flip (see flip()).
+ card.textContent = '';
+ onTap(card, function(){ flip(card); });
+ grid.appendChild(card);
+ });
+ statusEl.textContent = 'Tap a card to begin';
+ againBtn.classList.remove('visible');
+ }
+
+ function flip(card) {
+ if (card.classList.contains('flipped') || card.classList.contains('matched')) return;
+ if (flipped.length >= 2) return;
+ card.classList.add('flipped');
+ setFace(card, card.dataset.emoji);
+ flipped.push(card);
+ if (flipped.length === 2) {
+ moves++;
+ var a = flipped[0], b = flipped[1];
+ if (a.dataset.emoji === b.dataset.emoji) {
+ a.classList.add('matched');
+ b.classList.add('matched');
+ matched += 2;
+ flipped = [];
+ if (matched === deck.length) {
+ statusEl.textContent = '✓ Cleared in ' + moves + ' moves';
+ againBtn.classList.add('visible');
+ }
+ } else {
+ setTimeout(function(){
+ a.classList.remove('flipped');
+ b.classList.remove('flipped');
+ clearFace(a);
+ clearFace(b);
+ flipped = [];
+ }, 700);
+ }
+ }
+ }
+
+ onTap(againBtn, setup);
+ setup();
+})();
+</script>
+<script>/*ll-media-controls*/
+(function(){
+ function wire(){
+ var ll = window.liveloop; if(!ll || !ll.declareMedia) return;
+ var nodes = [].slice.call(document.querySelectorAll('video,audio')).filter(function(el){
+ return !(el.id && el.id.indexOf('ll-')===0) && !el.hasAttribute('data-ll-unmanaged');
+ });
+ if(!nodes.length) return;
+ var hasVideo = nodes.some(function(el){ return el.tagName==='VIDEO'; });
+ ll.declareMedia({ sound:true, playback:hasVideo });
+ var paused=false;
+ function play(el){ try{ var p=el.play&&el.play(); if(p&&p.catch)p.catch(function(){}); }catch(e){} }
+ function applyMuted(m){ nodes.forEach(function(el){ try{ el.muted=m; }catch(e){} if(!m&&!paused) play(el); }); }
+ function applyPaused(p){ paused=p; nodes.forEach(function(el){ try{ if(p){ el.pause&&el.pause(); } else { play(el); } }catch(e){} }); }
+ applyMuted(!!ll.muted);
+ if(ll.onMute) ll.onMute(applyMuted);
+ if(ll.onPause) ll.onPause(applyPaused);
+ }
+ if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', wire); } else { wire(); }
+})();
+</script>

v1Current

@liveloop · 5/15/2026, 2:13:08 PM

Initial version — all lines are new.

+<style>
+ html, body { background: #fef9c3; color: #111; font-family: system-ui, sans-serif; }
+ .wrap { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; gap: 0.75rem; padding: 1rem; }
+ h1 { margin: 0; font-size: 0.875rem; letter-spacing: 0.15em; text-transform: uppercase; color: #71717a; font-weight: 600; }
+ .grid { display: grid; gap: 0.5rem; }
+ /* Cards are real <button>s (reliable tap target) with the native
+ button chrome reset away. A face-down card is genuinely empty —
+ the emoji text is only added when flipped. The old approach hid
+ the emoji with color:transparent and revealed it by switching the
+ color back, which did not repaint emoji glyphs on mobile Safari
+ (flipped cards showed blank white). */
+ .card { appearance: none; -webkit-appearance: none; border: 0; margin: 0; padding: 0; font-family: inherit; color: inherit; aspect-ratio: 1; display: flex; align-items: center; justify-content: center; border-radius: 0.5rem; background: #fde047; font-size: 1.5rem; line-height: 1; cursor: pointer; user-select: none; -webkit-user-select: none; touch-action: manipulation; transition: background-color 0.15s; }
+ .card.flipped { background: #fff; }
+ .card .face-img { width: 82%; height: 82%; object-fit: cover; border-radius: 0.35rem; }
+ .card.matched { background: #86efac; cursor: default; }
+ .status { font-size: 0.875rem; color: #52525b; }
+ .again { padding: 0.5rem 1rem; border-radius: 9999px; border: 0; background: #18181b; color: white; font-weight: 500; cursor: pointer; display: none; }
+ .again.visible { display: inline-block; }
+</style>
+<div class="wrap">
+ <h1>Memory: emojis</h1>
+ <div class="grid" id="grid"></div>
+ <div class="status" id="status">Tap a card to begin</div>
+ <button class="again" id="again">Play again</button>
+</div>
+<script>
+(function(){
+ var pairs = ["🌟","🌈","🍕","🎲","🦊","🚀"];
+ function isImg(v){ return typeof v === 'string' && v.indexOf('https://') === 0; }
+ function setFace(card, v){
+ if (isImg(v)) {
+ var img = document.createElement('img');
+ img.src = v; img.alt = ''; img.className = 'face-img';
+ card.innerHTML = ''; card.appendChild(img);
+ } else { card.textContent = v; }
+ }
+ function clearFace(card){ card.innerHTML = ''; }
+ var grid = document.getElementById('grid');
+ var statusEl = document.getElementById('status');
+ var againBtn = document.getElementById('again');
+ var deck, flipped, matched, moves;
+
+ // Tap handling via pointer events instead of 'click'. Inside the
+ // scroll-snap feed on mobile, the browser frequently classifies a
+ // touch on a small target as a scroll and never synthesizes the
+ // 'click' — so cards looked dead. pointerdown/up fire regardless;
+ // we just confirm the finger barely moved (a tap, not a drag) and
+ // bail on pointercancel (the browser took the gesture for scroll).
+ function onTap(el, handler) {
+ var sx = 0, sy = 0, armed = false;
+ el.addEventListener('pointerdown', function(e){
+ armed = true; sx = e.clientX; sy = e.clientY;
+ });
+ el.addEventListener('pointercancel', function(){ armed = false; });
+ el.addEventListener('pointerup', function(e){
+ if (!armed) return;
+ armed = false;
+ if (Math.abs(e.clientX - sx) < 14 && Math.abs(e.clientY - sy) < 14) {
+ handler();
+ }
+ });
+ }
+
+ function setup() {
+ deck = pairs.concat(pairs).sort(function(){ return Math.random() - 0.5; });
+ flipped = [];
+ matched = 0;
+ moves = 0;
+ var cols = pairs.length <= 4 ? 4 : 4;
+ grid.style.gridTemplateColumns = 'repeat(' + cols + ', minmax(0, 1fr))';
+ grid.style.width = 'min(70vmin, 320px)';
+ grid.innerHTML = '';
+ deck.forEach(function(emoji, i){
+ var card = document.createElement('button');
+ card.type = 'button';
+ card.className = 'card';
+ card.dataset.idx = i;
+ card.dataset.emoji = emoji;
+ // Face-down: empty. The emoji is added on flip (see flip()).
+ card.textContent = '';
+ onTap(card, function(){ flip(card); });
+ grid.appendChild(card);
+ });
+ statusEl.textContent = 'Tap a card to begin';
+ againBtn.classList.remove('visible');
+ }
+
+ function flip(card) {
+ if (card.classList.contains('flipped') || card.classList.contains('matched')) return;
+ if (flipped.length >= 2) return;
+ card.classList.add('flipped');
+ setFace(card, card.dataset.emoji);
+ flipped.push(card);
+ if (flipped.length === 2) {
+ moves++;
+ var a = flipped[0], b = flipped[1];
+ if (a.dataset.emoji === b.dataset.emoji) {
+ a.classList.add('matched');
+ b.classList.add('matched');
+ matched += 2;
+ flipped = [];
+ if (matched === deck.length) {
+ statusEl.textContent = '✓ Cleared in ' + moves + ' moves';
+ againBtn.classList.add('visible');
+ }
+ } else {
+ setTimeout(function(){
+ a.classList.remove('flipped');
+ b.classList.remove('flipped');
+ clearFace(a);
+ clearFace(b);
+ flipped = [];
+ }, 700);
+ }
+ }
+ }
+
+ onTap(againBtn, setup);
+ setup();
+})();
+</script>
+<script>/*ll-media-controls*/
+(function(){
+ function wire(){
+ var ll = window.liveloop; if(!ll || !ll.declareMedia) return;
+ var nodes = [].slice.call(document.querySelectorAll('video,audio')).filter(function(el){
+ return !(el.id && el.id.indexOf('ll-')===0) && !el.hasAttribute('data-ll-unmanaged');
+ });
+ if(!nodes.length) return;
+ var hasVideo = nodes.some(function(el){ return el.tagName==='VIDEO'; });
+ ll.declareMedia({ sound:true, playback:hasVideo });
+ var paused=false;
+ function play(el){ try{ var p=el.play&&el.play(); if(p&&p.catch)p.catch(function(){}); }catch(e){} }
+ function applyMuted(m){ nodes.forEach(function(el){ try{ el.muted=m; }catch(e){} if(!m&&!paused) play(el); }); }
+ function applyPaused(p){ paused=p; nodes.forEach(function(el){ try{ if(p){ el.pause&&el.pause(); } else { play(el); } }catch(e){} }); }
+ applyMuted(!!ll.muted);
+ if(ll.onMute) ll.onMute(applyMuted);
+ if(ll.onPause) ll.onPause(applyPaused);
+ }
+ if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', wire); } else { wire(); }
+})();
+</script>
← Version history