Gallery Lightbox Stage
A dark frame shows large images at their original aspect ratio; switching zooms the image slightly as it fades in and the text rises. A floating control bar at the bottom has previous, page number, next and autoplay, with a thin progress line while playing and arrow key support.
Taken from the page template Yanbai Independent Designer Portfolio. Use it on its own, or combine it with the other components from the same template to rebuild the full page.
Plain HTML, CSS and a little vanilla JS, with no framework dependency. Copy the prompt into Claude Code, Cursor, Trae or Codex and the model adds Gallery Lightbox Stage to your project in your colors.
Where to use it
Image comparisons, video showcases, galleries, carousels and media cards.
Install
With Builder, install it with the 8pt CLI or call it directly from an editor connected to 8pt MCP.
Code
Source for Gallery Lightbox Stage. Motion uses only transform and opacity and respects the Reduce motion setting.
<div class="x-gls"><div class="x-gls__stage" tabindex="0" aria-label="作品大图"><span class="tin"></span><span class="x-gls__prog"></span></div><div class="x-gls__bar"><button type="button" class="x-gls__btn" data-step="-1" aria-label="上一张"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 5.5L8 12l6.5 6.5"/></svg></button><span class="x-gls__cnt">1 / 9</span><button type="button" class="x-gls__btn" data-step="1" aria-label="下一张"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9.5 5.5L16 12l-6.5 6.5"/></svg></button><span class="x-gls__sep"></span><button type="button" class="x-gls__btn x-gls__pp" aria-label="自动播放" aria-pressed="false"><svg class="i-play" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 5.5v13l10.5-6.5L8 5.5z"/></svg><svg class="i-pause" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 5.5v13"/><path d="M15.5 5.5v13"/></svg></button></div></div>
.x-gls{box-sizing:border-box;display:flex;flex-direction:column;align-items:center;gap:22px;width:100%;max-width:1040px;margin:0 auto;color:#efe9de;font-family:"PingFang SC","Noto Sans SC","Noto Sans CJK SC",system-ui,sans-serif}.x-gls *,.x-gls *::before,.x-gls *::after{box-sizing:border-box}.x-gls svg{width:1em;height:1em;flex:none}.x-gls__stage{position:relative;width:min(100%,calc(700px * var(--ar,1.7778)));padding:6px;border-radius:22px;background:#2a2725;outline:none;box-shadow:0 0 0 1px rgba(239,233,222,.09),0 40px 120px -20px rgba(0,0,0,.9);transition:width .5s cubic-bezier(.2,.8,.2,1)}.x-gls__stage:focus-visible{box-shadow:0 0 0 2px #f0b07a,0 40px 120px -20px rgba(0,0,0,.9)}.x-gls .tin{position:relative;display:block;width:100%;aspect-ratio:var(--ar,1.7778);border-radius:17px;overflow:hidden;background:#0e0d0c;container-type:inline-size}.x-gls .tin img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;animation:x-gls-in .6s cubic-bezier(.2,.8,.2,1)}.x-gls .tin .site{z-index:2;animation:x-gls-up .7s .05s cubic-bezier(.2,.8,.2,1) both}.x-gls .d .tin::before{content:"";position:absolute;inset:0;z-index:1;background:linear-gradient(180deg,rgba(0,0,0,.28),transparent 30%,transparent 55%,rgba(0,0,0,.45))}@keyframes x-gls-in{from{opacity:0;transform:scale(1.04)}}@keyframes x-gls-up{from{opacity:0;transform:translateY(10px)}}.x-gls__prog{position:absolute;left:24px;right:24px;bottom:14px;z-index:3;height:2px;border-radius:2px;background:rgba(255,255,255,.18);transform-origin:left;transform:scaleX(0);opacity:0}.x-gls__prog.is-run{opacity:1;animation:x-gls-prog 3.2s linear forwards}@keyframes x-gls-prog{from{transform:scaleX(0);background:rgba(255,255,255,.18)}to{transform:scaleX(1);background:rgba(255,255,255,.7)}}.x-gls__bar{display:flex;align-items:center;gap:4px;height:46px;padding:0 6px;border-radius:99px;background:rgba(32,29,27,.9);box-shadow:inset 0 0 0 1px rgba(239,233,222,.09);font-size:13px;font-variant-numeric:tabular-nums}.x-gls__btn{display:grid;place-items:center;width:34px;height:34px;border:0;border-radius:50%;background:transparent;color:#efe9de;font-size:16px;cursor:pointer;transition:background .2s}.x-gls__btn:hover{background:rgba(255,255,255,.08)}.x-gls__cnt{min-width:58px;text-align:center;color:#cfc6ba}.x-gls__sep{width:1px;height:18px;margin:0 4px;background:rgba(239,233,222,.09)}.x-gls__pp .i-pause{display:none}.x-gls__pp[aria-pressed="true"] .i-pause{display:block}.x-gls__pp[aria-pressed="true"] .i-play{display:none}@media (prefers-reduced-motion:reduce){.x-gls .tin img,.x-gls .tin .site,.x-gls__prog.is-run{animation:none}.x-gls__stage{transition:none}}.x-gls .site{position:absolute;inset:0;padding:5.2cqw 5.6cqw;display:flex;flex-direction:column;color:#fff;font-size:1.7cqw;line-height:1.4;text-align:left;pointer-events:none}.x-gls .lt .site{color:#1f1b17}.x-gls .s-top{display:flex;justify-content:space-between;align-items:flex-start;gap:3cqw;font-size:1.8cqw}.x-gls .s-logo{font-weight:700;letter-spacing:.06em}.x-gls .s-nav{display:flex;gap:2.8cqw;opacity:.78}.x-gls .s-nav i,.x-gls .s-cols i,.x-gls .s-foot i{font-style:normal}.x-gls .s-h{margin-top:auto;font-size:5.6cqw;line-height:1.14;font-weight:650;letter-spacing:.01em;text-shadow:0 .4cqw 2cqw rgba(0,0,0,.25)}.x-gls .lt .s-h{text-shadow:none}.x-gls .s-h2{font-size:3.4cqw;line-height:1.18;font-weight:650}.x-gls .s-cols{display:grid;grid-template-columns:repeat(3,auto);gap:.8cqw 4cqw;font-size:1.6cqw;opacity:.8}.x-gls .s-p{margin-top:1.4cqw;font-size:1.75cqw;opacity:.72;max-width:52%}.x-gls .s-btn{align-self:flex-start;margin-top:2.4cqw;padding:1.1cqw 2.5cqw;border-radius:99px;background:rgba(255,255,255,.94);color:#151311;font-size:1.65cqw;font-weight:600}.x-gls .s-dk{background:#1f1b17;color:#f4efe6}.x-gls .s-r{align-self:flex-end;text-align:right}.x-gls .s-c{align-self:center;text-align:center;max-width:none}.x-gls .s-cb{align-self:center}.x-gls .s-up{margin-top:6cqw;margin-bottom:auto}.x-gls .s-big{margin-top:auto;align-self:center;font-size:21cqw;font-weight:800;line-height:.9;color:#fff;letter-spacing:.04em;text-shadow:0 1cqw 4cqw rgba(30,60,20,.25)}.x-gls .s-foot{display:flex;justify-content:space-between;margin-top:2cqw;font-size:1.5cqw;opacity:.75;color:#fff}.x-gls .s-404{margin:auto auto 0;font-size:17cqw;font-weight:800;line-height:1;letter-spacing:.04em;color:rgba(255,255,255,.9);text-shadow:0 0 6cqw rgba(120,220,255,.35)}.x-gls .s-link{margin-top:2.4cqw;font-size:1.7cqw;opacity:.85}.x-gls .s-pill{padding:.8cqw 2cqw;border:1px solid rgba(255,255,255,.35);border-radius:99px;font-size:1.5cqw}.x-gls .s-hint{align-self:flex-end;margin-top:-2cqw;font-size:1.4cqw;opacity:.55}(root) => {
const C = [{"k": "lantern", "ar": 1.7778, "t": "d", "name": "灯屿 · 夜读", "img": "https://8pt.dev/api/media/596ca18adba2d1c8eb2b5f28.webp", "site": "<span class=\"s-top\"><b class=\"s-logo\">灯屿</b><span class=\"s-nav\"><i>夜读</i><i>札记</i><i>关于</i></span></span><span class=\"s-h\">把夜晚<br>留给慢下来的人</span><span class=\"s-p\">收藏值得读的文字,等夜深时再慢慢翻开</span><span class=\"s-btn\">进入夜读</span>"}, {"k": "bamboo", "ar": 1.7778, "t": "l", "name": "竹息 · 林间", "img": "https://8pt.dev/api/media/2a48eefd24317a44b0258c79.webp", "site": "<span class=\"s-top\"><span class=\"s-h2\">一处安静的<br>林间角落</span><span class=\"s-cols\"><i>白噪音</i><i>专注</i><i>日志</i><i>花园</i><i>关于</i><i>联系</i></span></span><span class=\"s-big\">竹息</span><span class=\"s-foot\"><i>© 竹息</i><i>隐私 · 条款</i></span>"}, {"k": "door", "ar": 1.7778, "t": "d", "name": "沙门 · 出口", "img": "https://8pt.dev/api/media/1a24aa1e7d9544d8f8a6da70.webp", "site": "<span class=\"s-top\"><b class=\"s-logo\">沙门</b><span class=\"s-nav\"><i>故事</i><i>出口</i></span></span><span class=\"s-h\">走出<br>信息流</span><span class=\"s-p\">推开门,世界会安静一点</span><span class=\"s-link\">点击进入 →</span>"}, {"k": "telescope", "ar": 1.3333, "t": "l", "name": "云镜 · 观测", "img": "https://8pt.dev/api/media/ec31ce9b0b61db0d2b2d1f92.webp", "site": "<span class=\"s-top\"><b class=\"s-logo\">云镜</b><span class=\"s-nav\"><i>观测</i><i>星图</i><i>订阅</i></span></span><span class=\"s-h s-r\">看得更远<br>一点点</span><span class=\"s-p s-r\">把日落之后的天空装进口袋</span>"}, {"k": "sunken", "ar": 1.7778, "t": "d", "name": "沉舟 · 404", "img": "https://8pt.dev/api/media/2a2e69f3d62322e9318bb4c6.webp", "site": "<span class=\"s-top\"><b class=\"s-logo\">沉舟</b><span class=\"s-nav\"><i>返回</i></span></span><span class=\"s-404\">404</span><span class=\"s-p s-c\">这一页沉到海底了</span><span class=\"s-btn s-cb\">浮回首页</span>"}, {"k": "globe", "ar": 1.3333, "t": "l", "name": "雪屋 · 冬日", "img": "https://8pt.dev/api/media/18decab4de8eda12bcd7ab66.webp", "site": "<span class=\"s-top\"><b class=\"s-logo\">雪屋</b><span class=\"s-nav\"><i>小屋</i><i>手作</i><i>候补</i></span></span><span class=\"s-h\">冬天的<br>小屋</span><span class=\"s-btn s-dk\">加入候补</span>"}, {"k": "clay", "ar": 0.75, "t": "l", "name": "陶序 · 器物", "img": "https://8pt.dev/api/media/b72329bcdf3f85eb9382a8ad.webp", "site": "<span class=\"s-top\"><b class=\"s-logo\">陶序</b><span class=\"s-nav\"><i>器物</i><i>工坊</i></span></span><span class=\"s-h\">器物<br>与留白</span><span class=\"s-p\">手捏的形状,安静的颜色</span>"}, {"k": "book", "ar": 1.7778, "t": "d", "name": "星页 · 阅读", "img": "https://8pt.dev/api/media/6e610c02b7c385e2fd9b5975.webp", "site": "<span class=\"s-top\"><b class=\"s-logo\">星页</b><span class=\"s-nav\"><i>书架</i><i>星图</i><i>我的</i></span></span><span class=\"s-h s-c s-up\">每一页<br>都是一片星空</span><span class=\"s-btn s-cb\">开始阅读</span>"}, {"k": "glass", "ar": 1.7778, "t": "d", "name": "棱光 · 发布", "img": "https://8pt.dev/api/media/44f2c620b7b4377af63ad5b3.webp", "site": "<span class=\"s-top\"><b class=\"s-logo\">棱光</b><span class=\"s-pill\">预约体验</span></span><span class=\"s-h\">一束光,<br>万种颜色</span><span class=\"s-p\">透过一块玻璃,看见光的全部</span><span class=\"s-hint\">向下浏览</span>"}];
const N = C.length;
const stage = root.querySelector('.x-gls__stage');
const tin = stage.querySelector('.tin');
const prog = root.querySelector('.x-gls__prog');
const cnt = root.querySelector('.x-gls__cnt');
const pp = root.querySelector('.x-gls__pp');
let cur = 0, timer = 0;
const mod = (a, b) => ((a % b) + b) % b;
function show(i) {
cur = mod(i, N);
…