🚀 返回顶部按钮 - 完整实现方案
针对 Hugo + Dream 主题 的完整返回顶部按钮实现,固定悬浮在页面右下角,支持滚动显示/隐藏、平滑滚动、深浅色适配。
📁 Partial 组件(推荐)
1️⃣ 创建 layouts/partials/back-to-top.html
{{/*
返回顶部按钮 - Dream 主题兼容版
功能:动态创建、固定右下角、滚动显示/隐藏、平滑滚动、深色模式、键盘辅助
配置:hugo.toml 中 params.backToTop
*/}}
{{ if .Site.Params.enableBackToTop }}
<script>
(function() {
// ========== 配置 ==========
const config = {
// 显示阈值:滚动超过多少像素显示按钮
showThreshold: {{ .Site.Params.backToTop.showThreshold | default 300 }},
// 滚动动画时长(毫秒)
scrollDuration: {{ .Site.Params.backToTop.scrollDuration | default 600 }},
// 按钮位置
position: {{ .Site.Params.backToTop.position | default "bottom-right" }}, // bottom-right | bottom-left
// 是否显示进度环(需额外 CSS)
showProgress: {{ .Site.Params.backToTop.showProgress | default false }},
// 主题颜色变量(适配 Dream 主题)
colors: {
bg: 'hsl(var(--p))',
text: 'hsl(var(--pc))',
border: 'hsl(var(--p) / 0.2)',
hoverBg: 'hsl(var(--p) / 0.9)',
}
};
// ========== 创建按钮 ==========
function createButton() {
// 检查是否已存在
if (document.getElementById('back-to-top')) return;
const btn = document.createElement('button');
btn.id = 'back-to-top';
btn.setAttribute('aria-label', '返回顶部');
btn.setAttribute('title', '按 Home 键也可返回顶部');
btn.setAttribute('tabindex', '0');
// 位置样式
const positionStyles = config.position === 'bottom-left'
? 'bottom: 24px !important; left: 24px !important; right: auto !important;'
: 'bottom: 24px !important; right: 24px !important; left: auto !important;';
// 应用样式(内联 + !important 确保优先级)
btn.style.cssText = `
position: fixed !important;
${positionStyles}
width: 48px !important;
height: 48px !important;
border-radius: 50% !important;
background-color: ${config.colors.bg} !important;
color: ${config.colors.text} !important;
border: 2px solid ${config.colors.border} !important;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease !important;
z-index: 999999 !important;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
cursor: pointer !important;
-webkit-tap-highlight-color: transparent !important;
user-select: none !important;
`;
// 图标 SVG
btn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 flex-shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
style="pointer-events: none; transition: transform 0.2s ease;">
<path stroke-linecap="round"
stroke-linejoin="round"
d="M5 10l7-7m0 0l7 7m-7-7v18" />
</svg>
`;
// 添加到 body(避免被父元素裁剪)
document.body.appendChild(btn);
// 初始化状态
btn.style.transform = 'translateY(20px) scale(0.9)';
return btn;
}
// ========== 滚动检测 ==========
function setupScrollHandler(btn) {
let ticking = false;
const threshold = config.showThreshold;
function checkScroll() {
const scrollY = window.scrollY || window.pageYOffset || document.documentElement.scrollTop;
if (scrollY > threshold) {
// 显示按钮
btn.style.opacity = '1';
btn.style.pointerEvents = 'auto';
btn.style.transform = 'translateY(0) scale(1)';
} else {
// 隐藏按钮
btn.style.opacity = '0';
btn.style.pointerEvents = 'none';
btn.style.transform = 'translateY(20px) scale(0.9)';
}
}
// 初始检查(防止页面加载时已在底部)
checkScroll();
// 滚动监听(passive 优化性能)
window.addEventListener('scroll', function() {
if (!ticking) {
window.requestAnimationFrame(function() {
checkScroll();
ticking = false;
});
ticking = true;
}
}, { passive: true });
return checkScroll;
}
// ========== 事件绑定 ==========
function bindEvents(btn) {
// 点击返回顶部
btn.addEventListener('click', function(e) {
e.preventDefault();
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
// 键盘支持:Enter/Space 触发,Home 键直接返回顶部
btn.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Home' && btn.style.pointerEvents !== 'none') {
e.preventDefault();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
});
// 悬停动画
btn.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-2px) scale(1.1)';
this.style.boxShadow = '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)';
this.style.backgroundColor = config.colors.hoverBg;
// 图标微动画
const icon = this.querySelector('svg');
if (icon) icon.style.transform = 'translateY(-2px)';
});
btn.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0) scale(1)';
this.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
this.style.backgroundColor = config.colors.bg;
const icon = this.querySelector('svg');
if (icon) icon.style.transform = 'translateY(0)';
});
// 触摸设备优化
btn.addEventListener('touchstart', function() {
this.style.transform = 'scale(0.95)';
}, { passive: true });
btn.addEventListener('touchend', function() {
this.style.transform = 'scale(1)';
}, { passive: true });
}
// ========== 深色模式适配 ==========
function setupThemeObserver(btn) {
// 初始适配
function adaptTheme() {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark' ||
window.matchMedia('(prefers-color-scheme: dark)').matches;
btn.style.backgroundColor = isDark
? 'hsl(var(--p) / 0.85) !important'
: config.colors.bg;
}
adaptTheme();
// 监听主题变化
const observer = new MutationObserver(adaptTheme);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme']
});
// 监听系统偏好变化
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', adaptTheme);
}
// ========== 移动端适配 ==========
function setupResponsive(btn) {
function adaptSize() {
if (window.innerWidth <= 640) {
btn.style.width = '44px';
btn.style.height = '44px';
btn.style.bottom = '16px';
btn.style.right = config.position === 'bottom-left' ? '16px' : '16px';
} else {
btn.style.width = '48px';
btn.style.height = '48px';
btn.style.bottom = '24px';
btn.style.right = config.position === 'bottom-left' ? '24px' : '24px';
}
}
adaptSize();
window.addEventListener('resize', adaptSize, { passive: true });
}
// ========== 初始化 ==========
function init() {
const btn = createButton();
if (!btn) return;
setupScrollHandler(btn);
bindEvents(btn);
setupThemeObserver(btn);
setupResponsive(btn);
// 调试日志(生产环境可移除)
if (window.location.hostname === 'localhost') {
console.log('✅ Back-to-top button initialized');
}
}
// 等待 DOM 就绪
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>
{{ end }}
2️⃣ 在 hugo.toml 中启用
[params]
# 启用返回顶部按钮
enableBackToTop = true
# 可选:自定义配置
[params.backToTop]
# 滚动超过多少像素显示按钮(默认 300)
showThreshold = 300
# 平滑滚动时长(毫秒,默认 600)
scrollDuration = 600
# 按钮位置:bottom-right | bottom-left(默认 bottom-right)
position = "bottom-right"
# 是否显示进度环(默认 false,需额外 CSS 支持)
showProgress = false
3️⃣ 在主题布局中引入
Dream 主题使用 baseof.html 作为基础模板,在 {{ block "main" . }} 之后添加:
覆盖 layouts/_default/baseof.html
# 复制主题的 baseof.html 到项目
cp themes/Dream/layouts/_default/baseof.html layouts/_default/baseof.html
然后在 </body> 标签前添加:
<!-- layouts/_default/baseof.html -->
<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode }}" data-theme="{{ .Site.Params.defaultTheme }}">
<head>
{{/* ... 原有 head 内容 ... */}}
</head>
<body class="flex flex-col min-h-screen">
{{/* ... 原有 body 内容 ... */}}
<main class="flex-grow">
{{ block "main" . }}{{ end }}
</main>
{{/* ... 原有 footer 等内容 ... */}}
{{/* 👇 添加返回顶部按钮 */}}
{{ partial "back-to-top.html" . }}
</body>
</html>
🚀 快速启用(5 分钟)
# 1. 创建 partial 文件
mkdir -p layouts/partials
cat > layouts/partials/back-to-top.html << 'EOF'
上述代码
EOF
# 2. 在 hugo.toml 中启用
echo -e "\n[params]\n enableBackToTop = true" >> hugo.toml
# 3. 在 layouts/_default/baseof.html 的 </body> 前添加
# {{ partial "back-to-top.html" . }}
# 4. 清理缓存并重启
hugo --gc --ignoreCache
hugo server -D --disableFastRender
✅ 最终效果
📱 初始状态(滚动 <300px):
(按钮隐藏)
📱 滚动后(>300px):
┌─────────┐
│ ↑ │ ← 圆形按钮,主题色
└─────────┘
🖱️ 悬停时:
┌─────────┐
│ ↑ │ ← 轻微放大 + 阴影
└─────────┘
~~~ ← 阴影增强
🌓 深色模式:
┌─────────┐
│ ↑ │ ← 自动适配深色背景
└─────────┘
📱 移动端 (<640px):
┌─────┐
│ ↑ │ ← 按钮稍小,位置内缩
└─────┘
- 📱 移动端自动调整位置
- 🌓 深浅色模式自动适配
- ⚡ 性能优化(requestAnimationFrame)
- ♿ 支持键盘操作和屏幕阅读器
- 🎨 平滑动画过渡
🔧 可选增强
1️⃣ 添加进度环(显示页面滚动进度)
在 btn.innerHTML 中添加 SVG 环:
btn.innerHTML = `
<svg class="absolute inset-0 w-full h-full -rotate-90" viewBox="0 0 36 36">
<path class="text-primary/20" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831"
fill="none" stroke="currentColor" stroke-width="3"/>
<path id="progress-ring" class="text-primary" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831"
fill="none" stroke="currentColor" stroke-width="3"
stroke-dasharray="100, 100" stroke-dashoffset="100"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 relative z-10" ...>
<path ... d="M5 10l7-7m0 0l7 7m-7-7v18" />
</svg>
`;
// 在 checkScroll 函数中添加进度更新
const progress = Math.min(100, (scrollY / (document.body.scrollHeight - window.innerHeight)) * 100);
const ring = btn.querySelector('#progress-ring');
if (ring) ring.style.strokeDashoffset = 100 - progress;
2️⃣ 添加点击音效(可选)
btn.addEventListener('click', function() {
// 轻微点击反馈
this.style.transform = 'scale(0.9)';
setTimeout(() => {
this.style.transform = 'translateY(0) scale(1)';
}, 150);
window.scrollTo({ top: 0, behavior: 'smooth' });
});
3️⃣ 添加统计(记录使用次数)
// 在 localStorage 中记录点击次数
btn.addEventListener('click', function() {
const count = parseInt(localStorage.getItem('backToTopClicks') || '0') + 1;
localStorage.setItem('backToTopClicks', count);
console.log(`📊 返回顶部已使用 ${count} 次`);
window.scrollTo({ top: 0, behavior: 'smooth' });
});
⚠️ 常见问题排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 按钮不显示 | enableBackToTop 未启用 | 检查 hugo.toml 配置 |
| 位置不对 | CSS 被覆盖 | 确认 !important 和 z-index |
| 点击无效 | 事件未绑定 | 检查 Console 是否有 JS 错误 |
| 深色模式不生效 | 主题变量未加载 | 确认 Dream 主题已正确初始化 |
| 移动端错位 | position: fixed 被 transform 影响 | 确保按钮直接添加到 document.body |
| 滚动卡顿 | 事件监听未优化 | 确认使用 passive: true 和 requestAnimationFrame |
✅ 最终验证
访问 http://localhost:1313/任意文章,滚动页面:
1️⃣ 滚动 >300px → 右下角出现圆形按钮
2️⃣ 悬停按钮 → 轻微放大 + 阴影增强
3️⃣ 点击按钮 → 平滑滚动到顶部
4️⃣ 按 Home 键 → 同样返回顶部
5️⃣ 切换深色模式 → 按钮颜色自动适配
6️⃣ 手机访问 → 按钮大小位置自适应
