夜雨聆风学习资料网

ARTICLE · 1149381

HTML 实现玻璃卡片滚动切换效果(附源码)

HTML 实现玻璃卡片滚动切换效果(附源码)

源码获取

私信回复玻璃卡片获取完整源码

演示效果

完整代码

<!doctype html><html lang="zh-CN"><head>  <meta charset="UTF-8" />  <meta name="viewport" content="width=device-width,initial-scale=1" />  <title>玻璃卡片滑轮切换</title>  <style>    * {      box-sizing: border-box    }    html,    body {      height: 100%;      margin: 0    }    body {      display: grid;      place-items: center;      overflow: hidden;      background: #000;      color: #fff;      font-family: Inter, "PingFang SC", "Microsoft YaHei", sans-serif    }    .scene {      position: relative;      width: min(100vw, 1100px);      height: min(76vw, 780px);      min-height: 560px;      overflow: hidden;      isolation: isolate;      background: radial-gradient(circle at 56% 43%, rgba(0, 255, 174, .18), transparent 34%), radial-gradient(circle at 72% 50%, rgba(0, 179, 158, .13), transparent 31%), linear-gradient(115deg, #010705, #000)    }    .scene:before {      content: "";      position: absolute;      inset: 0;      pointer-events: none;      background: radial-gradient(circle at 50% 50%, transparent 30%, rgba(0, 0, 0, .62) 100%)    }    .counter {      position: absolute;      top: 18px;      left: 16px;      z-index: 30;      padding: 7px 12px;      border: 1px solid rgba(255, 255, 255, .12);      border-radius: 20px;      background: rgba(0, 0, 0, .32);      color: rgba(255, 255, 255, .48);      font-size: 13px    }    .dots {      position: absolute;      top: 16px;      right: 30px;      display: flex;      gap: 7px;      z-index: 30    }    .dot {      width: 8px;      height: 8px;      padding: 0;      border: 0;      border-radius: 50%;      background: rgba(255, 255, 255, .3);      cursor: pointer;      transition: .35s    }    .dot.active {      width: 22px;      border-radius: 8px;      background: #50e4bd;      box-shadow: 0 0 18px rgba(80, 228, 189, .55)    }    .deck {      position: absolute;      inset: 65px 0 35px;      perspective: 1350px;      transform-style: preserve-3d    }    .card {      --x: 0;      --z: 0;      --ry: 0;      --scale: 1;      position: absolute;      left: 50%;      top: 50%;      width: 570px;      height: 630px;      padding: 42px 38px;      border-radius: 35px;      overflow: hidden;      transform: translate(-50%, -50%) translateX(var(--x)) translateZ(var(--z)) rotateY(var(--ry)) scale(var(--scale));      transform-origin: center;      transition: transform .72s cubic-bezier(.2, .8, .2, 1), opacity .55s, filter .55s;      cursor: pointer;      background: linear-gradient(145deg, rgba(36, 255, 185, .43), rgba(0, 98, 73, .7));      border: 2px solid rgba(181, 255, 231, .38);      box-shadow: inset 0 1px rgba(255, 255, 255, .35), inset 0 -40px 80px rgba(0, 48, 37, .2), 0 30px 75px rgba(0, 0, 0, .62), 0 0 45px rgba(0, 255, 180, .08);      backdrop-filter: blur(22px) saturate(150%);      -webkit-backdrop-filter: blur(22px) saturate(150%)    }    .card:before {      content: "";      position: absolute;      inset: 0;      border-radius: inherit;      pointer-events: none;      background: linear-gradient(125deg, rgba(255, 255, 255, .15), transparent 30%, transparent 70%, rgba(255, 255, 255, .05))    }    .card.active {      --x: -145px;      --z: 130px;      --ry: 0deg;      --scale: 1;      z-index: 20;      opacity: 1    }    .card.next-1 {      --x: 110px;      --z: 10px;      --ry: -3deg;      --scale: .9;      z-index: 15;      opacity: .66    }    .card.next-2 {      --x: 265px;      --z: -110px;      --ry: -5deg;      --scale: .78;      z-index: 14;      opacity: .48    }    .card.next-3 {      --x: 370px;      --z: -220px;      --ry: -7deg;      --scale: .67;      z-index: 13;      opacity: .36    }    .card.next-4 {      --x: 450px;      --z: -310px;      --ry: -9deg;      --scale: .58;      z-index: 12;      opacity: .29    }    .card.next-far {      --x: 520px;      --z: -390px;      --ry: -11deg;      --scale: .51;      z-index: 11;      opacity: .22    }    .card.prev-1 {      --x: -375px;      --z: -100px;      --ry: 8deg;      --scale: .8;      z-index: 10;      opacity: 0;      pointer-events: none    }    .card.hidden {      --x: 560px;      --z: -500px;      --ry: -12deg;      --scale: .46;      opacity: 0;      pointer-events: none    }    .card h2 {      margin: 0 0 12px;      font-size: 30px;      letter-spacing: -1px;      text-shadow: 0 2px 14px rgba(0, 0, 0, .25)    }    .card .quarter {      font-size: 19px;      color: #a4f6da;      font-weight: 700    }    .lines {      margin-top: 120px;      display: grid;      gap: 18px;      opacity: .2    }    .lines i {      display: block;      height: 3px;      background: linear-gradient(90deg, rgba(255, 255, 255, .75), transparent);      border-radius: 5px    }    .lines i:nth-child(2) {      width: 78%    }    .lines i:nth-child(3) {      width: 61%    }    .progress {      position: absolute;      left: 38px;      bottom: 70px;      display: flex;      align-items: flex-end;      gap: 14px    }    .progress strong {      font-size: 62px;      line-height: .9;      letter-spacing: -4px    }    .progress span {      font-size: 16px;      font-weight: 700;      color: #eafff8    }    .play {      position: absolute;      right: 28px;      bottom: 26px;      width: 72px;      height: 72px;      border-radius: 50%;      border: 2px solid rgba(255, 255, 255, .42);      background: linear-gradient(145deg, rgba(255, 255, 255, .27), rgba(255, 255, 255, .08));      box-shadow: inset 0 1px rgba(255, 255, 255, .45), 0 12px 30px rgba(0, 0, 0, .32);      display: grid;      place-items: center;      color: #fff;      cursor: pointer    }    .play:after {      content: "";      display: block;      margin-left: 5px;      border-left: 20px solid white;      border-top: 13px solid transparent;      border-bottom: 13px solid transparent    }    .hint {      position: absolute;      bottom: 12px;      left: 50%;      z-index: 40;      transform: translateX(-50%);      color: rgba(255, 255, 255, .35);      font-size: 12px;      opacity: 0;      transition: .3s    }    .scene:hover .hint {      opacity: 1    }    .flash {      animation: flash .5s    }    @keyframes flash {      50% {        box-shadow: 0 0 65px rgba(83, 255, 198, .7), inset 0 0 55px rgba(255, 255, 255, .16)      }    }    @media(max-width:760px) {      .scene {        height: 100vh;        min-height: 520px      }      .deck {        inset: 75px 0 45px      }      .card {        width: min(76vw, 430px);        height: min(108vw, 570px);        padding: 30px 25px;        border-radius: 26px      }      .card.active {        --x: -10vw      }      .card.next-1 {        --x: 26vw      }      .card.next-2 {        --x: 45vw      }      .card.next-3,      .card.next-4,      .card.next-far {        --x: 58vw      }      .card h2 {        font-size: 24px      }      .lines {        margin-top: 90px      }      .progress {        left: 26px;        bottom: 55px      }      .progress strong {        font-size: 46px      }      .play {        width: 58px;        height: 58px      }      .dots {        right: 18px      }      .hint {        display: none      }    }    /* Pixel-calibrated against the 576 × 431 reference frame */    html,    body {      width: 100%;      height: 100%    }    .scene {      width: min(100vw, calc(100vh * 1.33643));      height: auto;      min-height: 0;      max-height: 100vh;      aspect-ratio: 576/431;      background: #000    }    .counter {      top: 6px;      left: 4px    }    .dots {      top: 7px;      right: 31px    }    .deck {      perspective: 1600px;      perspective-origin: 38% 50%    }    .card {      left: 50%;      top: 50%;      width: clamp(180px, 52%, 300px);      height: clamp(220px, 86%, 340px);      padding: 16px;      border-radius: 20px;      transform: translate(-50%, -50%) translate3d(var(--x), var(--y), var(--z)) rotateY(var(--ry)) scale(var(--scale));      transform-origin: center;      transition: transform .58s cubic-bezier(.34, 1.35, .64, 1), opacity .42s ease, background .38s ease, border-color .38s ease;      will-change: transform;      background: linear-gradient(160deg, rgba(255, 255, 255, .16) 0%, rgba(255, 255, 255, .04) 50%, rgba(180, 200, 220, .06) 100%);      border: 1px solid rgba(255, 255, 255, .22);      box-shadow: inset 0 1px 0 rgba(255, 255, 255, .35), inset 0 -1px 0 rgba(0, 0, 0, .15), 0 20px 40px rgba(0, 0, 0, .45);      backdrop-filter: blur(28px) saturate(140%);      -webkit-backdrop-filter: blur(28px) saturate(140%)    }    .card:before {      display: none    }    .card.active {      --x: -56px;      --y: 0px;      --z: 0px;      --ry: -8deg;      --scale: 1.08;      z-index: 7;      opacity: 1;      background: linear-gradient(155deg, rgba(52, 211, 153, .55) 0%, rgba(16, 185, 129, .75) 40%, rgba(4, 120, 87, .85) 100%);      border-color: rgba(167, 243, 208, .55);      box-shadow: inset 0 1px 0 rgba(255, 255, 255, .45), 0 0 40px rgba(16, 185, 129, .45), 0 24px 50px rgba(0, 0, 0, .5)    }    .card.next-1 {      --x: 24px;      --y: 1.5px;      --z: -58px;      --ry: -29.4deg;      --scale: .955;      z-index: 6;      opacity: .85    }    .card.next-2 {      --x: 66px;      --y: 3px;      --z: -116px;      --ry: -30.8deg;      --scale: .91;      z-index: 5;      opacity: .76    }    .card.next-3 {      --x: 108px;      --y: 4.5px;      --z: -174px;      --ry: -32.2deg;      --scale: .865;      z-index: 4;      opacity: .67    }    .card.next-4 {      --x: 150px;      --y: 6px;      --z: -232px;      --ry: -33.6deg;      --scale: .82;      z-index: 3;      opacity: .58    }    .card.next-far {      --x: 192px;      --y: 7.5px;      --z: -290px;      --ry: -35deg;      --scale: .775;      z-index: 2;      opacity: .49    }    .card.next-6 {      --x: 234px;      --y: 9px;      --z: -348px;      --ry: -36.4deg;      --scale: .76;      z-index: 1;      opacity: .4    }    .hit-rail {      position: absolute;      inset: 0;      z-index: 28;      display: block;      pointer-events: none    }    .card-hit {      position: absolute;      top: 12%;      bottom: 10%;      padding: 0;      border: 0;      outline: 0;      color: transparent;      background: transparent !important;      box-shadow: none !important;      filter: none !important;      backdrop-filter: none !important;      -webkit-backdrop-filter: none !important;      appearance: none;      -webkit-appearance: none;      -webkit-tap-highlight-color: transparent;      pointer-events: auto;      cursor: pointer    }    .card-hit:hover,    .card-hit:focus,    .card-hit:focus-visible,    .card-hit:active {      color: transparent;      background: transparent !important;      border: 0;      outline: 0;      box-shadow: none !important;      filter: none !important    }    .card-hit:nth-child(1) {      left: 67%;      width: 12%    }    .card-hit:nth-child(2) {      left: 79%;      width: 6%    }    .card-hit:nth-child(3) {      left: 85%;      width: 6%    }    .card-hit:nth-child(4) {      left: 91%;      width: 6%    }    .card-hit:nth-child(5) {      left: 97%;      width: 3%    }    .card:not(.active) .play {      width: 40px;      height: 40px    }    .card:not(.active) h2,    .card:not(.active) .quarter,    .card:not(.active) .progress {      opacity: .5    }    .deck:before,    .deck:after {      display: none !important;      content: none !important    }    .deck:before {      left: 18%;      top: 10%;      width: 65%;      height: 80%;      background: rgba(52, 211, 153, .20);      filter: blur(90px);      animation: pulseGlow 4.5s ease-in-out infinite    }    .deck:after {      right: 6%;      top: 25%;      width: 160px;      height: 160px;      background: rgba(34, 211, 238, .10);      filter: blur(60px)    }    .card {      background: linear-gradient(160deg, rgba(175, 235, 225, .22) 0%, rgba(65, 160, 155, .12) 55%, rgba(8, 60, 60, .10) 100%);      border-color: rgba(205, 255, 245, .24);      backdrop-filter: blur(18px) saturate(130%);      -webkit-backdrop-filter: blur(18px) saturate(130%)    }    .card.active {      background: linear-gradient(145deg, rgba(27, 168, 116, .48) 0%, rgba(12, 157, 110, .56) 46%, rgba(5, 111, 78, .68) 100%);      backdrop-filter: none !important;      -webkit-backdrop-filter: none !important;      transform-style: flat;      isolation: isolate    }    .card:not(.active) h2,    .card:not(.active) .quarter,    .card:not(.active) .progress {      opacity: .76    }    .card h2 {      font-size: clamp(15px, 2.8vw, 18px);      margin: 0    }    .card .quarter {      font-size: 11px    }    .lines {      margin-top: 34%;      gap: 10px    }    .lines i {      height: 2px    }    .progress {      left: 16px;      bottom: 58px    }    .progress strong {      font-size: clamp(36px, 6vw, 40px);      letter-spacing: -2px    }    .progress span {      font-size: 11px    }    .play {      right: 14px;      bottom: 14px;      width: 48px;      height: 48px    }    .play:after {      border-left-width: 14px;      border-top-width: 9px;      border-bottom-width: 9px    }    @media(max-width:760px) {      .scene {        height: auto;        min-height: 0      }      .deck {        inset: 7.8% 0 8%;        perspective: 1600px;        perspective-origin: 38% 50%      }      .card {        width: clamp(180px, 52%, 300px);        height: clamp(220px, 86%, 340px);        padding: 16px;        border-radius: 20px      }      .card.active {        --x: -56px;        --y: 0px;        --z: 0px;        --ry: -8deg;        --scale: 1.08      }      .card.next-1 {        --x: 24px;        --y: 1.5px;        --z: -58px;        --ry: -29.4deg;        --scale: .955      }      .card.next-2 {        --x: 66px;        --y: 3px;        --z: -116px;        --ry: -30.8deg;        --scale: .91      }      .card.next-3 {        --x: 108px;        --y: 4.5px;        --z: -174px;        --ry: -32.2deg;        --scale: .865      }      .card.next-4 {        --x: 150px;        --y: 6px;        --z: -232px;        --ry: -33.6deg;        --scale: .82      }      .card.next-far {        --x: 192px;        --y: 7.5px;        --z: -290px;        --ry: -35deg;        --scale: .775      }      .card.next-6 {        --x: 234px;        --y: 9px;        --z: -348px;        --ry: -36.4deg;        --scale: .76      }      .card h2 {        font-size: 16px      }      .lines {        margin-top: 34%      }      .progress {        left: 16px;        bottom: 58px      }      .progress strong {        font-size: 40px      }      .play {        width: 48px;        height: 48px      }      .dots {        right: 31px      }    }    .hit-rail {      display: none !important    }  </style></head><body>  <section class="scene" id="scene" tabindex="0" aria-label="文档卡片轮播">    <div class="counter">滚轮切换 · <b id="current">5</b>/<span id="total">7</span></div>    <nav class="dots" id="dots" aria-label="选择卡片"></nav>    <div class="deck" id="deck"></div>    <div class="hit-rail" id="hitRail" aria-label="点击后排卡片切换"></div>    <div class="hint">滚动鼠标滚轮 / 拖动 / 使用方向键</div>  </section>  <script>    const data = [      ['需求分析', '2025 - Q1', 40], ['产品路线图 3.0', '2025 - Q1', 70], ['交互原型', '2025 - Q1', 87],      ['视觉规范', '2025 - Q2', 65], ['测试用例集', '2025 - Q2', 80], ['发布计划', '2025 - Q2', 92], ['项目复盘', '2025 - Q2', 100]    ];    const scene = document.querySelector('#scene'), deck = document.querySelector('#deck'), dots = document.querySelector('#dots'), current = document.querySelector('#current'), hitRail = document.querySelector('#hitRail');    let active = 4, busy = false, startX = 0, wheelAcc = 0, wheelLock = false, autoPlay = true, autoTimer;    data.forEach((item, i) => {      const card = document.createElement('article'); card.className = 'card'; card.innerHTML = `<h2>${item[0]}</h2><div class="quarter">${item[1]}</div><div class="lines"><i></i><i></i><i></i></div><div class="progress"><strong>${item[2]}%</strong><span>完成度</span></div><button class="play" aria-label="播放 ${item[0]}"></button>`;      card.dataset.index = i; card.addEventListener('click', e => { e.stopPropagation(); if (i !== active) { go(i); return } if (e.target.classList.contains('play')) { card.classList.remove('flash'); void card.offsetWidth; card.classList.add('flash') } }); deck.append(card);      const dot = document.createElement('button'); dot.className = 'dot'; dot.setAttribute('aria-label', `第 ${i + 1} 张`); dot.onclick = () => go(i); dots.append(dot);    });    const cards = [...deck.children], dotList = [...dots.children], hitList = [];    for (let d = 1; d <= 6; d++) { const hit = document.createElement('button'); hit.className = 'card-hit'; hit.addEventListener('click', e => { e.stopPropagation(); go(Number(hit.dataset.index)) }); hitRail.append(hit); hitList.push(hit) }    function updateHits() { const sr = scene.getBoundingClientRect(); let right = cards[active].getBoundingClientRect().right; hitList.forEach((hit, i) => { const index = (active + i + 1) % data.length, r = cards[index].getBoundingClientRect(), start = Math.max(sr.left, right), end = Math.min(sr.right, r.right); hit.dataset.index = index; hit.setAttribute('aria-label', `切换到 ${data[index][0]}`); hit.style.left = `${start - sr.left}px`; hit.style.top = `${Math.max(0, r.top - sr.top)}px`; hit.style.width = `${Math.max(0, end - start)}px`; hit.style.height = `${Math.max(0, Math.min(sr.bottom, r.bottom) - Math.max(sr.top, r.top))}px`; hit.style.bottom = 'auto'; hit.style.display = end > start ? 'block' : 'none'; right = Math.max(right, r.right) }) }    function render() { cards.forEach((card, i) => { const d = (i - active + data.length) % data.length; card.className = 'card ' + (d === 0 ? 'active' : d === 1 ? 'next-1' : d === 2 ? 'next-2' : d === 3 ? 'next-3' : d === 4 ? 'next-4' : d === 5 ? 'next-far' : 'next-6'); card.setAttribute('aria-current', d === 0 ? 'true' : 'false') }); dotList.forEach((d, i) => d.classList.toggle('active', i === active)); current.textContent = active + 1; requestAnimationFrame(updateHits); setTimeout(updateHits, 650) }    function go(i) { active = (i + data.length) % data.length; render() }    function step(dir) { if (wheelLock) return; wheelLock = true; go(active + dir); setTimeout(() => wheelLock = false, 320) }    scene.addEventListener('wheel', e => { e.preventDefault(); e.stopPropagation(); wheelAcc += e.deltaY; if (wheelLock || Math.abs(wheelAcc) < 28) return; const dir = wheelAcc > 0 ? 1 : -1; wheelAcc = 0; step(dir) }, { passive: false });    scene.addEventListener('keydown', e => { if (e.key === 'ArrowRight' || e.key === 'ArrowDown') step(1); if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') step(-1) });    scene.addEventListener('click', e => { const ar = cards[active].getBoundingClientRect(); if (e.clientX <= ar.right) return; let right = ar.right; for (let d = 1; d < data.length; d++) { const index = (active + d) % data.length, r = cards[index].getBoundingClientRect(); if (e.clientX > right && e.clientX <= r.right) { e.preventDefault(); e.stopPropagation(); go(index); return } right = Math.max(right, r.right) } }, true);    scene.addEventListener('pointerdown', e => { startX = e.clientX }); scene.addEventListener('pointerup', e => { const dx = e.clientX - startX; if (Math.abs(dx) > 35) { e.preventDefault(); step(dx < 0 ? 1 : -1) } });    const startAuto = () => { clearInterval(autoTimer); if (autoPlay) autoTimer = setInterval(() => go(active + 1), 4500) }; scene.addEventListener('mouseenter', () => { autoPlay = false; clearInterval(autoTimer) }); scene.addEventListener('mouseleave', () => { autoPlay = true; startAuto() }); window.addEventListener('resize', updateHits); render(); startAuto(); scene.focus();  </script></body></html>

相关学习资料