Skip to content

Instantly share code, notes, and snippets.

@marstamyan
Created July 4, 2026 17:52
Show Gist options
  • Select an option

  • Save marstamyan/7b285628236b35a2200185f6ed4cfed4 to your computer and use it in GitHub Desktop.

Select an option

Save marstamyan/7b285628236b35a2200185f6ed4cfed4 to your computer and use it in GitHub Desktop.
Draggable 3-Row Tag Reel — glassmorphism
<!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