纯DOM复刻Chrome默认音频播放器


在网页中,音频播放是一个再经典不过的需求。通常我们直接用 <audio src="https://example.com/example.mp3" controls></audio> 就能搞定。但只要你对 UI 有一点点追求,很快就会遇到两个让人抓狂的痛点:

  1. 多端样式分裂:Chrome 的样式圆润现代,Firefox 的复古方正,Safari 又自成一派。在不同浏览器渲染下样式完全不一样,可能用 Chrome 打开的人觉得很好看就留下了,Firefox 打开的人一看一个现代页面中居然穿插了个梦回千禧年的播放器直接劝退。
  2. 完全无法定制:因为浏览器默认的播放控件被藏在影子 DOM (Shadow DOM) 内部,外界的 CSS 和 JS 根本摸不到它。如果你想在播放器里加个新的菜单选项、做点别的修改,根本不可能。

而在我音乐空间的2026.4.22更新中,我解决了这两个问题,既然我改不了 Shadow DOM,那就隐藏它,用 DOM 复刻一个 Chrome 默认的音频播放器。

效果对比:

对比图
注:上方为 DOM 复刻版,下方为 Chrome 原生播放器。

猛地一瞅,视觉和动态交互体验几乎和Chrome 原生播放器没区别。除了右侧展开的倍速菜单长得不同,其他几乎一致。

这个DOM复刻完美复刻了这两个交互细节:

  1. 自动向左滑出展开的音量条,Chrome 的音量条平时是隐藏的,鼠标悬停到喇叭图标上时,音量滑动条会平滑地向左滑出。

  2. 倍速菜单的自动滚动定位,当用户点击“播放速度”打开子菜单时,如果当前已经设置了 2.0x 播放,菜单会自动把当前选中的选项滚动到正中间。

完整实现代码

不依赖任何第三方库(如 jQuery、Tailwind 等),用纯原生 HTML5、CSS3 和 Vanilla JS 编写。你可以直接复制这些代码,在本地新建一个 .html 文件即可运行体验。

HTML里有个小细节,我给进度显示的cap-time加上了translate=”no”属性,这个属性的意思是让翻译插件滚开,避免说翻译插件把 00:00 / 00:00这个进度给翻译了。有的机翻会翻译乱掉,比如翻译成00:00、00时00分

HTML

<!-- 使用audio播放音乐,但隐藏浏览器自己渲染的默认播放器控件 -->
<audio id="PlayAudio" src="https://example.com/example.mp3"></audio>

<!-- 复刻DOM -->
<div class="chrome-audio-player" id="chromeAudioPlayer">
    <!-- 播放/暂停按钮 -->
    <button id="capPlayPauseBtn" class="cap-btn" title="播放">
        <img id="capPlayIcon" src="data:image/svg+xml;base64,PHN2ZyBmaWxsPSJXaW5kb3dUZXh0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxwYXRoIGQ9Ik04IDV2MTRsMTEtN3oiLz4KICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4KPC9zdmc+" alt="play">
        <img id="capPauseIcon" src="data:image/svg+xml;base64,PHN2ZyBmaWxsPSJXaW5kb3dUZXh0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxwYXRoIGQ9Ik02IDE5aDRWNUg2djE0em04LTE0djE0aDRWNWgtNHoiLz4KICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4KPC9zdmc+" style="display: none;" alt="pause">
    </button>
    
    <!-- 时间 -->
    <div class="cap-time" translate="no">
        <span id="capCurrentTime">0:00</span> / <span id="capDuration">0:00</span>
    </div>
    
    <!-- 进度条 -->
    <div class="cap-progress-container">
        <div id="capSeekSlider" class="cap-custom-slider">
            <div class="cap-track">
                <div id="capSeekFill" class="cap-fill"></div>
            </div>
            <div id="capSeekThumb" class="cap-thumb"></div>
        </div>
    </div>
    
    <!-- 音量 -->
    <div class="cap-volume-group">
        <button id="capMuteBtn" class="cap-btn" title="静音">
            <img id="capVolumeIcon" src="data:image/svg+xml;base64,PHN2ZyBmaWxsPSJXaW5kb3dUZXh0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxwYXRoIGQ9Ik0zIDl2Nmg0bDUgNVY0TDcgOUgzem0xMy41IDNjMC0xLjc3LTEuMDItMy4yOS0yLjUtNC4wM3Y4LjA1YzEuNDgtLjczIDIuNS0yLjI1IDIuNS00LjAyek0xNCAzLjIzdjIuMDZjMi44OS44NiA1IDMuNTQgNSA2Ljcxcy0yLjExIDUuODUtNSA2LjcxdjIuMDZjNC4wMS0uOTEgNy00LjQ5IDctOC43N3MtMi45OS03Ljg2LTctOC43N3oiLz4KICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4KPC9zdmc+" alt="volume">
            <img id="capMuteIcon" src="data:image/svg+xml;base64,PHN2ZyBmaWxsPSJXaW5kb3dUZXh0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxwYXRoIGQ9Ik0xNi41IDEyYzAtMS43Ny0xLjAyLTMuMjktMi41LTQuMDN2Mi4yMWwyLjQ1IDIuNDVjLjAzLS4yLjA1LS40MS4wNS0uNjN6bTIuNSAwYzAgLjk0LS4yIDEuODItLjU0IDIuNjRsMS41MSAxLjUxQzIwLjYzIDE0LjkxIDIxIDEzLjUgMjEgMTJjMC00LjI4LTIuOTktNy44Ni03LTguNzd2Mi4wNmMyLjg5Ljg2IDUgMy41NCA1IDYuNzF6TTQuMjcgM0wzIDQuMjcgNy43MyA5SDN2Nmg0bDUgNXYtNi43M2w0LjI1IDQuMjVjLS42Ny41Mi0xLjQyLjkzLTIuMjUgMS4xOHYyLjA2YzEuMzgtLjMxIDIuNjMtLjk1IDMuNjktMS44MUwxOS43MyAyMSAyMSAxOS43M2wtOS05TDQuMjcgM3pNMTIgNEw5LjkxIDYuMDkgMTIgOC4xOFY0eiIvPgogICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPgo8L3N2Zz4=" style="display: none;" alt="mute">
        </button>
        <div class="cap-volume-panel">
            <div id="capVolumeSlider" class="cap-custom-slider cap-vol-slider">
                <div class="cap-track">
                    <div id="capVolumeFill" class="cap-fill" style="width:100%"></div>
                </div>
                <div id="capVolumeThumb" class="cap-thumb" style="left:100%"></div>
            </div>
        </div>
    </div>
    
    <!-- 更多选项按钮 -->
    <div class="cap-menu-wrapper">
        <button id="capMoreBtn" class="cap-btn" title="更多选项">
            <img src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgMjQgMjQiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDI0IDI0OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgZmlsbD0iV2luZG93VGV4dCI+CjxnPgoJPGNpcmNsZSBjeD0iMTIiIGN5PSI2IiByPSIyIi8+Cgk8Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIyIi8+Cgk8Y2lyY2xlIGN4PSIxMiIgY3k9IjE4IiByPSIyIi8+CjwvZz4KPC9zdmc+" alt="more">
        </button>
        
        <!-- 菜单 -->
        <div id="capMainMenu" class="cap-popover hidden">
            <!-- 下载 -->
            <div class="cap-menu-item" id="capDownloadBtn">
                <img src="data:image/svg+xml;base64,PHN2ZyBmaWxsPSJXaW5kb3dUZXh0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxwYXRoIGQ9Ik0xOSA5aC00VjNIOXY2SDVsNyA3IDctN3pNNSAxOHYyaDE0di0ySDV6Ii8+CiAgICA8cGF0aCBkPSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIi8+Cjwvc3ZnPg==" class="cap-menu-icon" alt="download">
                <span>下载</span>
            </div>
            <!-- 倍速菜单选项 -->
            <div class="cap-menu-item" id="capOpenSpeedBtn">
                <img src="data:image/svg+xml;base64,PHN2ZyBmaWxsPSJXaW5kb3dUZXh0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHdpZHRoPSIyNCI+CiAgPHBhdGggZD0iTTAgMGgyNHYyNEgwVjB6IiBmaWxsPSJub25lIi8+CiAgPHBhdGggZD0iTTEzLjA1IDkuNzlMMTAgNy41djlsMy4wNS0yLjI5TDE2IDEybC0yLjk1LTIuMjF6bTAgMEwxMCA3LjV2OWwzLjA1LTIuMjlMMTYgMTJsLTIuOTUtMi4yMXptMCAwTDEwIDcuNXY5bDMuMDUtMi4yOUwxNiAxMmwtMi45NS0yLjIxek0xMSA0LjA3VjIuMDVjLTIuMDEuMi0zLjg0IDEtNS4zMiAyLjIxTDcuMSA1LjY5QTcuOTQxIDcuOTQxIDAgMDExMSA0LjA3ek01LjY5IDcuMUw0LjI2IDUuNjhBOS45NDkgOS45NDkgMCAwMDIuMDUgMTFoMi4wMmMuMTgtMS40Ni43Ni0yLjc5IDEuNjItMy45ek00LjA3IDEzSDIuMDVjLjIgMi4wMSAxIDMuODQgMi4yMSA1LjMybDEuNDMtMS40M0E3Ljg2OCA3Ljg2OCAwIDAxNC4wNyAxM3ptMS42MSA2Ljc0QTkuOTgxIDkuOTgxIDAgMDAxMSAyMS45NXYtMi4wMmE3Ljk0MSA3Ljk0MSAwIDAxLTMuOS0xLjYybC0xLjQyIDEuNDN6TTIyIDEyYzAgNS4xNi0zLjkyIDkuNDItOC45NSA5Ljk1di0yLjAyQzE2Ljk3IDE5LjQxIDIwIDE2LjA1IDIwIDEycy0zLjAzLTcuNDEtNi45NS03LjkzVjIuMDVDMTguMDggMi41OCAyMiA2Ljg0IDIyIDEyeiIvPgo8L3N2Zz4=" class="cap-menu-icon" alt="speed">
                <span>播放速度</span>
                <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzIwMjEyNCI+PHBhdGggZD0iTTguNTkgMTYuNTlMMTMuMTcgMTIgOC41OSA3LjQxIDEwIDZsNiA2LTYgNnoiLz48L3N2Zz4=" class="cap-arrow-icon" alt="arrow">
            </div>
        </div>

        <!-- 倍速选项子菜单 -->
        <div id="capSpeedMenu" class="cap-popover hidden cap-speed-popover">
            <!-- 返回主菜单 -->
            <div class="cap-menu-item cap-menu-header" id="capBackToMainBtn">
                <img src="data:image/svg+xml;base64,PHN2ZyBmaWxsPSJXaW5kb3dUZXh0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4KICAgIDxwYXRoIGQ9Ik0yMCAxMUg3LjgzbDUuNTktNS41OUwxMiA0bC04IDggOCA4IDEuNDEtMS40MUw3LjgzIDEzSDIwdi0yeiIvPgo8L3N2Zz4=" class="cap-menu-icon" alt="back">
                <span>播放速度</span>
            </div>
            <div class="cap-divider"></div>
            <!-- 倍速选项 -->
            <div class="cap-speed-list" id="capSpeedOptions"></div>
        </div>
    </div>
</div>

JavaScript

const audioPlayer = document.getElementById('PlayAudio');
const capPlayPauseBtn = document.getElementById('capPlayPauseBtn');
const capPlayIcon = document.getElementById('capPlayIcon');
const capPauseIcon = document.getElementById('capPauseIcon');
const capCurrentTime = document.getElementById('capCurrentTime');
const capDuration = document.getElementById('capDuration');
const capMuteBtn = document.getElementById('capMuteBtn');
const capVolumeIcon = document.getElementById('capVolumeIcon');
const capMuteIcon = document.getElementById('capMuteIcon');
const capMoreBtn = document.getElementById('capMoreBtn');
const capMainMenu = document.getElementById('capMainMenu');
const capSpeedMenu = document.getElementById('capSpeedMenu');
const capDownloadBtn = document.getElementById('capDownloadBtn');
const capOpenSpeedBtn = document.getElementById('capOpenSpeedBtn');
const capBackToMainBtn = document.getElementById('capBackToMainBtn');
const capSpeedOptionsContainer = document.getElementById('capSpeedOptions');

let previousVolume = 1;

function capFormatTime(seconds) {
    if (isNaN(seconds) || !isFinite(seconds)) return "0:00";
    const m = Math.floor(seconds / 60);
    const s = Math.floor(seconds % 60);
    return `${m}:${s < 10 ? '0' : ''}${s}`;
}

function setupCustomSlider(containerId, fillId, thumbId, onChange) {
    const container = document.getElementById(containerId);
    const fill = document.getElementById(fillId);
    const thumb = document.getElementById(thumbId);
    let isDragging = false;

    function update(e) {
        const rect = container.getBoundingClientRect();
        let clientX = e.clientX;
        if (e.touches && e.touches.length > 0) {
            clientX = e.touches[0].clientX;
        }
        if (clientX === undefined) return;

        let percentage = (clientX - rect.left) / rect.width;
        percentage = Math.max(0, Math.min(1, percentage));

        fill.style.width = `${percentage * 100}%`;
        thumb.style.left = `${percentage * 100}%`;

        if (onChange) onChange(percentage, isDragging);
    }

    function onDown(e) {
        isDragging = true;
        container.classList.add('dragging');
        update(e);
        document.addEventListener('mousemove', onMove);
        document.addEventListener('mouseup', onUp);
        document.addEventListener('touchmove', onMove, { passive: false });
        document.addEventListener('touchend', onUp);
    }

    function onMove(e) {
        if (!isDragging) return;
        e.preventDefault();
        update(e);
    }

    function onUp(e) {
        isDragging = false;
        container.classList.remove('dragging');
        update(e);
        document.removeEventListener('mousemove', onMove);
        document.removeEventListener('mouseup', onUp);
        document.removeEventListener('touchmove', onMove);
        document.removeEventListener('touchend', onUp);
    }

    container.addEventListener('mousedown', onDown);
    container.addEventListener('touchstart', onDown, { passive: false });

    return function setPercentage(pct) {
        if (!isDragging) {
            const safePct = Math.max(0, Math.min(1, pct));
            fill.style.width = `${safePct * 100}%`;
            thumb.style.left = `${safePct * 100}%`;
        }
    };
}

const updateSeekUI = setupCustomSlider('capSeekSlider', 'capSeekFill', 'capSeekThumb', (pct, dragging) => {
    if (dragging) {
        capCurrentTime.textContent = capFormatTime(pct * (audioPlayer.duration || 0));
    } else {
        audioPlayer.currentTime = pct * (audioPlayer.duration || 0);
    }
});

const updateVolumeUI = setupCustomSlider('capVolumeSlider', 'capVolumeFill', 'capVolumeThumb', (pct) => {
    audioPlayer.muted = false;
    audioPlayer.volume = pct;
    if (pct > 0) previousVolume = pct;
});

audioPlayer.addEventListener('loadedmetadata', () => {
    capDuration.textContent = capFormatTime(audioPlayer.duration);
    updateSeekUI(audioPlayer.currentTime / audioPlayer.duration);
    updateVolumeUI(audioPlayer.volume);
});

audioPlayer.addEventListener('timeupdate', () => {
    if (audioPlayer.duration) {
        capCurrentTime.textContent = capFormatTime(audioPlayer.currentTime);
        updateSeekUI(audioPlayer.currentTime / audioPlayer.duration);
    }
});

audioPlayer.addEventListener('play', () => {
    capPlayIcon.style.display = 'none';
    capPauseIcon.style.display = 'block';
});
audioPlayer.addEventListener('pause', () => {
    capPlayIcon.style.display = 'block';
    capPauseIcon.style.display = 'none';
});

audioPlayer.addEventListener('volumechange', () => {
    if (audioPlayer.volume === 0 || audioPlayer.muted) {
        capVolumeIcon.style.display = 'none';
        capMuteIcon.style.display = 'block';
        updateVolumeUI(0);
    } else {
        capVolumeIcon.style.display = 'block';
        capMuteIcon.style.display = 'none';
        updateVolumeUI(audioPlayer.volume);
    }
});

capPlayPauseBtn.addEventListener('click', () => {
    audioPlayer.paused ? audioPlayer.play() : audioPlayer.pause();
});

capMuteBtn.addEventListener('click', () => {
    if (audioPlayer.volume > 0 && !audioPlayer.muted) {
        previousVolume = audioPlayer.volume;
        audioPlayer.volume = 0;
        audioPlayer.muted = true;
    } else {
        audioPlayer.muted = false;
        audioPlayer.volume = previousVolume > 0 ? previousVolume : 1;
    }
});

document.addEventListener('click', (e) => {
    if (!capMoreBtn.contains(e.target) && !capMainMenu.contains(e.target) && !capSpeedMenu.contains(e.target)) {
        capMainMenu.classList.add('hidden');
        capSpeedMenu.classList.add('hidden');
    }
});

capMoreBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    if (capMainMenu.classList.contains('hidden') && capSpeedMenu.classList.contains('hidden')) {
        capMainMenu.classList.remove('hidden');
    } else {
        capMainMenu.classList.add('hidden');
        capSpeedMenu.classList.add('hidden');
    }
});

capOpenSpeedBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    capMainMenu.classList.add('hidden');
    capSpeedMenu.classList.remove('hidden');
    const activeOption = capSpeedMenu.querySelector('.cap-speed-option.active');
    if (activeOption) {
        const menuHalfHeight = capSpeedMenu.clientHeight / 2;
        const optionHalfHeight = activeOption.clientHeight / 2;
        capSpeedMenu.scrollTop = activeOption.offsetTop - menuHalfHeight + optionHalfHeight;
    }
});

capBackToMainBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    capSpeedMenu.classList.add('hidden');
    capMainMenu.classList.remove('hidden');
});

capDownloadBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    capMainMenu.classList.add('hidden');
    if (!audioPlayer.src) {
        console.warn("当前音频源为空");
        return;
    }
    
    const a = document.createElement('a');
    a.href = audioPlayer.src;
    const filename = audioPlayer.src.split('/').pop() || 'audio.mp3';
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
});

const speedRates = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];

function renderSpeedOptions() {
    capSpeedOptionsContainer.innerHTML = '';
    speedRates.forEach(rate => {
        const div = document.createElement('div');
        div.className = `cap-speed-option ${rate === audioPlayer.playbackRate ? 'active' : ''}`;
        div.textContent = rate === 1 ? '正常' : rate;
        
        div.addEventListener('click', (e) => {
            e.stopPropagation();
            audioPlayer.playbackRate = rate;
            renderSpeedOptions();
            capSpeedMenu.classList.add('hidden');
        });
        capSpeedOptionsContainer.appendChild(div);
    });
}

renderSpeedOptions();

CSS

*,
*::before,
*::after {
    box-sizing: border-box;
}
#chromeAudioPlayer.chrome-audio-player {
    display: flex;
    align-items: center;
    background-color: #f1f3f4;
    border-radius: 30px;
    height: 54px;
    padding: 0 6px;
    width: 100%;
    box-sizing: border-box;
    font-family: Roboto, Arial, sans-serif;
    color: #202124;
}
#chromeAudioPlayer.chrome-audio-player button.cap-btn,
.chrome-audio-player .cap-btn {
    background: transparent;
    border: none;
    box-shadow: none;
    border-radius: 50%;
    width: 40px;
    height: 40px;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
    outline: none;
    flex-shrink: 0;
    padding: 0;
    transition: background-color 0.2s;
}
#chromeAudioPlayer.chrome-audio-player button.cap-btn:hover { 
    background-color: rgba(0, 0, 0, 0.06); 
}
#chromeAudioPlayer.chrome-audio-player button.cap-btn:active { 
    background-color: rgba(0, 0, 0, 0.1); 
}
.chrome-audio-player .cap-btn img {
    width: 20px;
    height: 20px;
    opacity: 0.8;
    pointer-events: none;
}
.chrome-audio-player .cap-time {
    font-size: 14px;
    margin: 0 10px 0 0;
    user-select: none;
    flex-shrink: 0;
    min-width: 65px;
    text-align: center;
    letter-spacing: 0.2px;
}
.chrome-audio-player .cap-progress-container {
    flex: 1;
    margin: 0 10px;
}
.chrome-audio-player .cap-custom-slider {
    position: relative;
    width: 100%;
    height: 24px;
    cursor: pointer;
    display: flex;
    align-items: center;
    touch-action: none;
}
.chrome-audio-player .cap-track {
    width: 100%;
    height: 4px;
    background: #dfdfdf;
    border-radius: 2px;
    pointer-events: none;
}
.chrome-audio-player .cap-fill {
    height: 100%;
    width: 0%;
    background: #202124;
    border-radius: 2px;
}
.chrome-audio-player .cap-thumb {
    width: 10px;
    height: 10px;
    background: transparent;
    border-radius: 50%;
    position: absolute;
    top: 50%;
    left: 0%;
    transform: translate(-50%, -50%);
    transition: background 0.2s, transform 0.1s;
    pointer-events: none;
}
.chrome-audio-player .cap-custom-slider:hover .cap-thumb,
.chrome-audio-player .cap-custom-slider.dragging .cap-thumb {
    background: #202124;
}
.chrome-audio-player .cap-custom-slider.dragging .cap-thumb {
    transform: translate(-50%, -50%) scale(1.3);
}
.chrome-audio-player .cap-volume-group {
    display: flex;
    align-items: center;
    flex-direction: row-reverse;
}
.chrome-audio-player .cap-volume-panel {
    width: 0;
    opacity: 0;
    overflow: hidden;
    display: flex;
    align-items: center;
    transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease;
}
.chrome-audio-player .cap-vol-slider {
    width: 60px;
    margin-right: 5px;
}
.chrome-audio-player .cap-volume-group:hover .cap-volume-panel {
    width: 60px;
    opacity: 1;
}
.chrome-audio-player .cap-menu-wrapper { 
    position: relative; 
}
.chrome-audio-player .cap-popover {
    position: absolute;
    bottom: 50px;
    right: 0;
    background: #fff;
    border-radius: 4px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
    min-width: 160px;
    padding: 8px 0;
    z-index: 100;
    font-size: 13px;
}
.chrome-audio-player .cap-speed-popover { 
    min-width: 160px; 
    max-height: 250px; 
    overflow-y: auto; 
}
.chrome-audio-player .cap-popover.hidden { 
    display: none; 
}
.chrome-audio-player .cap-menu-item {
    display: flex;
    align-items: center;
    padding: 10px 16px;
    cursor: pointer;
    color: #202124;
}
.chrome-audio-player .cap-menu-item:hover { 
    background: #f1f3f4; 
}
.chrome-audio-player .cap-menu-icon { 
    width: 18px; 
    height: 18px; 
    margin-right: 12px; 
    opacity: 0.7; 
    pointer-events: none;
}
.chrome-audio-player .cap-arrow-icon { 
    width: 16px; 
    height: 16px; 
    margin-left: auto; 
    opacity: 0.5; 
    pointer-events: none;
}
.chrome-audio-player .cap-divider { 
    height: 1px; 
    background: #e0e0e0; 
    margin: 8px 0; 
}
.chrome-audio-player .cap-speed-option {
    display: flex;
    align-items: center;
    padding: 10px 40px 10px 16px;
    cursor: pointer;
    position: relative;
}
.chrome-audio-player .cap-speed-option:hover { 
    background: #f1f3f4; 
}
.chrome-audio-player .cap-speed-option.active::before {
    content: "";
    position: absolute;
    right: 18px;
    width: 18px;
    height: 18px;
    background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23202124'%3e%3cpath d='M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z'/%3e%3c/svg%3e");
    background-size: contain;
    background-repeat: no-repeat;
    background-position: center;
}


文章作者: Ethaniel
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Ethaniel !
赏
  目录