Created
July 4, 2026 17:52
-
-
Save marstamyan/7b285628236b35a2200185f6ed4cfed4 to your computer and use it in GitHub Desktop.
Draggable 3-Row Tag Reel — glassmorphism
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="ru"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Tag Reel</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet"> | |
| <!-- ============================================================ | |
| TAG REEL — plain HTML + CSS + JS, no dependencies. | |
| To embed in your project, copy three things: | |
| 1) the <style> below (tagreel-* classes) | |
| 2) the <div class="tagreel-frame" data-tagreel> ... </div> block | |
| 3) the <script> at the end of the page | |
| Tags are plain <a> elements inside the block — edit them right | |
| in the HTML; the script lays them out across the rows for you. | |
| ============================================================ --> | |
| <style> | |
| html, body { margin: 0; } | |
| /* demo page background — not needed in your project */ | |
| body { | |
| min-height: 100vh; | |
| background: #0a1220; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 56px 40px; | |
| box-sizing: border-box; | |
| } | |
| /* the widget */ | |
| .tagreel-frame { | |
| position: relative; | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | |
| border-radius: 24px; | |
| background: radial-gradient(130% 130% at 50% 0%, #132238 0%, #08111f 62%, #050b15 100%); | |
| border: 1px solid rgba(255,255,255,0.07); | |
| box-shadow: 0 34px 90px rgba(0,0,0,0.5); | |
| overflow: hidden; | |
| cursor: grab; | |
| touch-action: none; | |
| } | |
| .tagreel-pill { | |
| position: absolute; | |
| left: 0; top: 0; | |
| white-space: nowrap; | |
| padding: 10px 22px; | |
| border-radius: 999px; | |
| color: #fff; | |
| text-decoration: none; | |
| font-weight: 500; | |
| font-size: 16px; | |
| letter-spacing: 0.2px; | |
| background: linear-gradient(180deg, rgba(255,255,255,0.12), rgba(255,255,255,0.045)); | |
| border: 1px solid rgba(255,255,255,0.13); | |
| box-shadow: 0 6px 20px rgba(0,0,0,0.28), inset 0 1px 0 rgba(255,255,255,0.10); | |
| transform-origin: center center; | |
| will-change: transform, opacity; | |
| user-select: none; | |
| -webkit-user-drag: none; | |
| cursor: pointer; | |
| transition: background 0.2s ease, border-color 0.2s ease; | |
| } | |
| .tagreel-pill:hover { | |
| background: linear-gradient(180deg, rgba(255,255,255,0.24), rgba(255,255,255,0.11)); | |
| border-color: rgba(255,255,255,0.32); | |
| } | |
| /* soft fade on the left & right edges */ | |
| .tagreel-edge { | |
| position: absolute; inset: 0; pointer-events: none; | |
| background: linear-gradient(90deg, #0a1424 0%, rgba(10,20,36,0) 14%, rgba(10,20,36,0) 86%, #0a1424 100%); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="tagreel-frame" data-tagreel style="width:940px; max-width:94vw; height:280px;"> | |
| <a class="tagreel-pill" href="https://developer.mozilla.org/docs/Web/JavaScript" target="_blank" rel="noopener noreferrer" draggable="false">JavaScript</a> | |
| <a class="tagreel-pill" href="https://developer.mozilla.org/docs/Web/HTML" target="_blank" rel="noopener noreferrer" draggable="false">HTML</a> | |
| <a class="tagreel-pill" href="https://developer.mozilla.org/docs/Web/CSS" target="_blank" rel="noopener noreferrer" draggable="false">CSS</a> | |
| <a class="tagreel-pill" href="https://react.dev" target="_blank" rel="noopener noreferrer" draggable="false">React</a> | |
| <a class="tagreel-pill" href="https://vuejs.org" target="_blank" rel="noopener noreferrer" draggable="false">Vue</a> | |
| <a class="tagreel-pill" href="https://angular.dev" target="_blank" rel="noopener noreferrer" draggable="false">Angular</a> | |
| <a class="tagreel-pill" href="https://nodejs.org" target="_blank" rel="noopener noreferrer" draggable="false">Node.js</a> | |
| <a class="tagreel-pill" href="https://www.typescriptlang.org" target="_blank" rel="noopener noreferrer" draggable="false">TypeScript</a> | |
| <a class="tagreel-pill" href="https://nextjs.org" target="_blank" rel="noopener noreferrer" draggable="false">Next.js</a> | |
| <a class="tagreel-pill" href="https://www.mongodb.com" target="_blank" rel="noopener noreferrer" draggable="false">MongoDB</a> | |
| <a class="tagreel-pill" href="https://graphql.org" target="_blank" rel="noopener noreferrer" draggable="false">GraphQL</a> | |
| <a class="tagreel-pill" href="https://www.docker.com" target="_blank" rel="noopener noreferrer" draggable="false">Docker</a> | |
| <a class="tagreel-pill" href="https://kubernetes.io" target="_blank" rel="noopener noreferrer" draggable="false">Kubernetes</a> | |
| <a class="tagreel-pill" href="https://www.python.org" target="_blank" rel="noopener noreferrer" draggable="false">Python</a> | |
| <a class="tagreel-pill" href="https://www.rust-lang.org" target="_blank" rel="noopener noreferrer" draggable="false">Rust</a> | |
| <a class="tagreel-pill" href="https://go.dev" target="_blank" rel="noopener noreferrer" draggable="false">Go</a> | |
| <a class="tagreel-pill" href="https://svelte.dev" target="_blank" rel="noopener noreferrer" draggable="false">Svelte</a> | |
| <a class="tagreel-pill" href="https://redux.js.org" target="_blank" rel="noopener noreferrer" draggable="false">Redux</a> | |
| <a class="tagreel-pill" href="https://webpack.js.org" target="_blank" rel="noopener noreferrer" draggable="false">Webpack</a> | |
| <a class="tagreel-pill" href="https://vitejs.dev" target="_blank" rel="noopener noreferrer" draggable="false">Vite</a> | |
| <a class="tagreel-pill" href="https://tailwindcss.com" target="_blank" rel="noopener noreferrer" draggable="false">Tailwind</a> | |
| <a class="tagreel-pill" href="https://www.postgresql.org" target="_blank" rel="noopener noreferrer" draggable="false">PostgreSQL</a> | |
| <a class="tagreel-pill" href="https://redis.io" target="_blank" rel="noopener noreferrer" draggable="false">Redis</a> | |
| <a class="tagreel-pill" href="https://aws.amazon.com" target="_blank" rel="noopener noreferrer" draggable="false">AWS</a> | |
| <a class="tagreel-pill" href="https://www.figma.com" target="_blank" rel="noopener noreferrer" draggable="false">Figma</a> | |
| <a class="tagreel-pill" href="https://sass-lang.com" target="_blank" rel="noopener noreferrer" draggable="false">Sass</a> | |
| <a class="tagreel-pill" href="https://expressjs.com" target="_blank" rel="noopener noreferrer" draggable="false">Express</a> | |
| <a class="tagreel-pill" href="https://deno.com" target="_blank" rel="noopener noreferrer" draggable="false">Deno</a> | |
| <a class="tagreel-pill" href="https://www.prisma.io" target="_blank" rel="noopener noreferrer" draggable="false">Prisma</a> | |
| <a class="tagreel-pill" href="https://jestjs.io" target="_blank" rel="noopener noreferrer" draggable="false">Jest</a> | |
| <div class="tagreel-edge"></div> | |
| </div> | |
| <script> | |
| (function () { | |
| var ROWS = 3; // number of rows | |
| var GAP = 26; // gap between pills, px | |
| var ROWGAP = 78; // gap between rows, px | |
| var AUTO = 0.35; // auto-scroll speed, px/frame (0 = off) | |
| var instances = []; | |
| function setup(frame) { | |
| var pills = Array.prototype.slice.call(frame.querySelectorAll('.tagreel-pill')); | |
| if (!pills.length) return; | |
| var s = { offset: 0, vel: 0, dragging: false, didDrag: false, | |
| lastX: 0, downX: 0, raf: null, onScreen: true }; | |
| // distribute pills across rows, then measure positions | |
| var rows = []; | |
| for (var r = 0; r < ROWS; r++) rows.push({ els: [] }); | |
| pills.forEach(function (el, i) { | |
| el.addEventListener('dragstart', function (e) { e.preventDefault(); }); | |
| rows[i % ROWS].els.push(el); | |
| }); | |
| for (var r = 0; r < ROWS; r++) { | |
| var row = rows[r], acc = 0; | |
| row.pos = []; | |
| for (var i = 0; i < row.els.length; i++) { | |
| var w = row.els[i].offsetWidth; | |
| row.pos.push(acc + w / 2); | |
| acc += w + GAP; | |
| } | |
| row.total = acc; | |
| row.stagger = (r * acc) / (ROWS + 0.5); // offset rows so they don't line up | |
| } | |
| function render() { | |
| var W = frame.clientWidth, H = frame.clientHeight, cx = W / 2; | |
| var cy0 = H / 2 - ROWGAP; | |
| for (var r = 0; r < ROWS; r++) { | |
| var row = rows[r], cy = cy0 + r * ROWGAP, half = row.total / 2; | |
| for (var i = 0; i < row.els.length; i++) { | |
| var raw = row.pos[i] + row.stagger - s.offset; | |
| raw = ((raw % row.total) + row.total) % row.total; | |
| if (raw > half) raw -= row.total; | |
| var d = Math.min(1, Math.abs(raw) / half); | |
| var el = row.els[i]; | |
| el.style.transform = 'translate3d(' + (cx + raw) + 'px,' + cy + 'px,0) translate(-50%,-50%)'; | |
| el.style.opacity = Math.max(0, 1 - d * 1.1); | |
| } | |
| } | |
| } | |
| function loop() { | |
| s.raf = requestAnimationFrame(loop); | |
| if (s.dragging) return; // while dragging, move() handles rendering | |
| s.offset += s.vel; | |
| s.vel += (AUTO - s.vel) * 0.04; // ease velocity back to cruising speed | |
| render(); | |
| } | |
| function update() { // start / pause the loop | |
| var should = s.onScreen && !document.hidden; | |
| if (should && !s.raf) s.raf = requestAnimationFrame(loop); | |
| else if (!should && s.raf) { cancelAnimationFrame(s.raf); s.raf = null; } | |
| } | |
| // --- input: mouse + touch --- | |
| function pt(e) { return e.touches ? e.touches[0] : e; } | |
| function down(e) { | |
| s.dragging = true; s.didDrag = false; s.vel = 0; | |
| var p = pt(e); s.lastX = s.downX = p.clientX; | |
| frame.style.cursor = 'grabbing'; | |
| } | |
| function move(e) { | |
| if (!s.dragging) return; | |
| var p = pt(e), dx = p.clientX - s.lastX; s.lastX = p.clientX; | |
| if (Math.abs(p.clientX - s.downX) > 5) s.didDrag = true; | |
| s.offset -= dx; s.vel = -dx; render(); | |
| if (e.cancelable) e.preventDefault(); | |
| } | |
| function up() { | |
| if (!s.dragging) return; | |
| s.dragging = false; frame.style.cursor = 'grab'; | |
| } | |
| frame.addEventListener('mousedown', down); | |
| frame.addEventListener('touchstart', down, { passive: true }); | |
| window.addEventListener('mousemove', move); | |
| window.addEventListener('touchmove', move, { passive: false }); | |
| window.addEventListener('mouseup', up); | |
| window.addEventListener('touchend', up); | |
| frame.addEventListener('wheel', function (e) { | |
| e.preventDefault(); | |
| s.offset += (Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY) * 0.7; | |
| render(); | |
| }, { passive: false }); | |
| // ignore the click if it was actually a drag | |
| frame.addEventListener('click', function (e) { | |
| if (s.didDrag) { e.preventDefault(); e.stopPropagation(); } | |
| }, true); | |
| // pause when the widget scrolls off-screen | |
| if ('IntersectionObserver' in window) { | |
| new IntersectionObserver(function (en) { | |
| s.onScreen = en[0].isIntersecting; update(); | |
| }, { threshold: 0 }).observe(frame); | |
| } | |
| s.update = update; | |
| instances.push(s); | |
| render(); | |
| update(); | |
| } | |
| // pause when the tab is hidden (one listener for all widgets) | |
| document.addEventListener('visibilitychange', function () { | |
| instances.forEach(function (s) { s.update(); }); | |
| }); | |
| function boot() { | |
| var frames = document.querySelectorAll('[data-tagreel]'); | |
| Array.prototype.forEach.call(frames, setup); | |
| } | |
| // wait for the font to load, otherwise pill widths measure wrong | |
| if (document.fonts && document.fonts.ready) document.fonts.ready.then(boot); | |
| else window.addEventListener('load', boot); | |
| })(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment