跳转到内容

收起侧边栏

为 Starlight 侧边栏添加悬浮/常驻切换功能

Section titled “为 Starlight 侧边栏添加悬浮/常驻切换功能”

本教程将介绍如何为 Astro Starlight 文档站点的侧边栏添加悬浮模式和常驻模式切换功能。实现效果包括:

  • 悬浮模式:侧边栏默认收起,鼠标悬停时展开
  • 常驻模式:侧边栏固定展开
  • 切换按钮:位于侧边栏 “Welcome to Starlight” 导航项右侧,使用 ☰ 符号
  • 主题适配:按钮颜色自动跟随深色/浅色主题变化
  • 模式记忆:使用 localStorage 保存用户偏好

需要创建/修改以下文件:

├── public/
│ └── scripts/
│ └── sidebar.js # 侧边栏功能脚本
└── src/
└── styles/
└── custom.css # 自定义样式

src/styles/custom.css 中添加以下样式:

/* 侧边栏收起/展开功能自定义样式 */
/* 侧边栏容器基础样式 */
:root {
--sidebar-width: 300px;
--sidebar-collapsed-width: 0px;
--sidebar-transition-duration: 0.3s;
}
/* 侧边栏包装器 - 使用 Starlight 的 sidebar-pane 类 */
.sidebar-pane {
position: fixed;
top: var(--header-height, 64px);
left: 0;
height: calc(100vh - var(--header-height, 64px));
width: var(--sidebar-width);
z-index: 1000;
transition: transform var(--sidebar-transition-duration) ease,
width var(--sidebar-transition-duration) ease;
}
/* 悬浮模式 - 默认收起 */
body.sidebar-floating .sidebar-pane {
width: var(--sidebar-width);
transform: translateX(calc(-100% + 40px));
}
/* 悬浮模式下 hover 时展开 */
body.sidebar-floating .sidebar-pane:hover,
body.sidebar-floating .sidebar-pane.sidebar-open {
transform: translateX(0);
}
/* 常驻模式 */
body.sidebar-docked .sidebar-pane {
transform: translateX(0);
}
/* 侧边栏内容 - 禁用滚动 */
.sidebar-content {
height: 100%;
background-color: var(--color-sidebarBackground);
border-right: 1px solid var(--color-hairline);
overflow-y: hidden;
overflow-x: hidden;
}
/* 导航项容器 - 为切换按钮提供定位上下文 */
.sl-sidebar-nav li:first-child a,
.sl-sidebar-nav a[href="/"],
.sidebar-pane a[href="/"],
nav a[href="/"] {
position: relative !important;
}
/* 切换按钮 - 使用 CSS 伪元素创建,与导航项一起加载 */
.sl-sidebar-nav li:first-child a::after,
.sl-sidebar-nav a[href="/"]::after,
.sidebar-pane a[href="/"]::after,
nav a[href="/"]::after {
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 8px;
width: 28px;
height: 28px;
background-color: var(--color-accent);
border: 1px solid var(--color-hairline);
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
transition: all 0.2s ease;
font-size: 18px;
font-weight: 700;
color: var(--color-sidebarItemText);
line-height: 1;
}
/* 悬停状态 */
.sl-sidebar-nav li:first-child a:hover::after,
.sl-sidebar-nav a[href="/"]:hover::after,
.sidebar-pane a[href="/"]:hover::after,
nav a[href="/"]:hover::after {
background-color: var(--color-accent);
transform: translateY(-50%) scale(1.05);
}
/* 常驻模式:显示☰符号(水平) */
body.sidebar-docked .sl-sidebar-nav li:first-child a::after,
body.sidebar-docked .sl-sidebar-nav a[href="/"]::after,
body.sidebar-docked .sidebar-pane a[href="/"]::after,
body.sidebar-docked nav a[href="/"]::after {
content: '';
transform: translateY(-50%) rotate(0deg);
}
/* 悬浮模式:显示☰符号(旋转 90 度,垂直) */
body.sidebar-floating .sl-sidebar-nav li:first-child a::after,
body.sidebar-floating .sl-sidebar-nav a[href="/"]::after,
body.sidebar-floating .sidebar-pane a[href="/"]::after,
body.sidebar-floating nav a[href="/"]::after {
content: '';
transform: translateY(-50%) rotate(90deg);
}
/* 移动端适配 */
@media (max-width: 768px) {
.sidebar-pane {
width: 100%;
transform: translateX(-100%);
}
body.sidebar-floating .sidebar-pane,
body.sidebar-docked .sidebar-pane {
transform: translateX(-100%);
}
body.sidebar-floating .sidebar-pane.sidebar-open,
body.sidebar-docked .sidebar-pane.sidebar-open {
transform: translateX(0);
}
/* 移动端隐藏切换按钮 */
.sl-sidebar-nav li:first-child a::after,
.sl-sidebar-nav a[href="/"]::after,
.sidebar-pane a[href="/"]::after,
nav a[href="/"]::after {
display: none;
}
}
/* 主内容区域适配 */
.main-content-wrapper,
.main-content {
margin-left: var(--sidebar-width);
transition: margin-left var(--sidebar-transition-duration) ease;
}
/* 悬浮模式下主内容占满全屏 */
body.sidebar-floating .main-content-wrapper,
body.sidebar-floating .main-content {
margin-left: 0;
}
/* 完全隐藏滚动条但保持滚动功能 */
.sidebar-content {
scrollbar-width: none;
-ms-overflow-style: none;
}
.sidebar-content::-webkit-scrollbar {
width: 0;
height: 0;
}
.sidebar-content::-webkit-scrollbar-track {
background: transparent;
}
.sidebar-content::-webkit-scrollbar-thumb {
background: transparent;
}
/* Starlight 侧边栏导航样式 */
starlight-layout .sidebar,
.sl-layout .sidebar {
position: relative;
height: 100%;
}

public/scripts/sidebar.js 中添加以下代码:

/**
* 侧边栏收起/展开功能脚本
* 实现侧边栏的悬浮模式和常驻模式切换
* 针对 Starlight 框架优化
*/
(function () {
'use strict';
const STORAGE_KEY = 'starlight-sidebar-mode';
const MODE_FLOATING = 'floating';
const MODE_DOCKED = 'docked';
/**
* 查找侧边栏元素
*/
function findSidebarElement() {
const selectors = [
'.sidebar-pane',
'.sidebar',
'.starlight-aside',
'aside',
'[role="navigation"]'
];
for (const selector of selectors) {
const element = document.querySelector(selector);
if (element) {
console.log('[Sidebar] Found sidebar using selector:', selector);
return element;
}
}
return null;
}
/**
* 等待侧边栏元素出现
*/
function waitForSidebarElement(maxAttempts = 10) {
return new Promise((resolve) => {
let attempts = 0;
const check = () => {
const element = findSidebarElement();
if (element) {
resolve(element);
} else if (attempts++ < maxAttempts) {
setTimeout(check, 100);
} else {
resolve(null);
}
};
check();
});
}
/**
* 初始化侧边栏功能
*/
function initSidebar() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupSidebar);
} else {
setupSidebar();
}
}
/**
* 设置侧边栏
*/
async function setupSidebar() {
console.log('[Sidebar] Setting up sidebar...');
const sidebarElement = await waitForSidebarElement();
if (!sidebarElement) {
console.warn('[Sidebar] Sidebar element not found');
return;
}
sidebarElement.classList.add('sl-sidebar-wrapper');
const savedMode = localStorage.getItem(STORAGE_KEY) || MODE_FLOATING;
setSidebarMode(savedMode);
const welcomeNavItem = findWelcomeNavItem();
if (welcomeNavItem) {
welcomeNavItem.addEventListener('click', function (e) {
const rect = welcomeNavItem.getBoundingClientRect();
const clickX = e.clientX;
const clickY = e.clientY;
const buttonRight = rect.right - 8;
const buttonLeft = buttonRight - 28;
const buttonTop = rect.top + (rect.height / 2) - 14;
const buttonBottom = buttonTop + 28;
if (clickX >= buttonLeft && clickX <= buttonRight &&
clickY >= buttonTop && clickY <= buttonBottom) {
e.preventDefault();
e.stopPropagation();
const mode = getCurrentMode();
const newMode = mode === MODE_FLOATING ? MODE_DOCKED : MODE_FLOATING;
setSidebarMode(newMode);
localStorage.setItem(STORAGE_KEY, newMode);
console.log('[Sidebar] Toggle clicked, new mode:', newMode);
}
});
}
setupHoverBehavior(sidebarElement);
console.log('[Sidebar] Setup complete, mode:', savedMode);
}
/**
* 查找导航项
*/
function findWelcomeNavItem() {
const selectors = [
'.sl-sidebar-nav li:first-child a',
'.sl-sidebar-nav a[href="/"]',
'.sidebar-pane a[href="/"]',
'nav a[href="/"]'
];
for (const selector of selectors) {
const element = document.querySelector(selector);
if (element) {
console.log('[Sidebar] Found welcome nav item using selector:', selector);
return element;
}
}
return null;
}
/**
* 获取当前模式
*/
function getCurrentMode() {
return document.body.classList.contains('sidebar-docked') ? MODE_DOCKED : MODE_FLOATING;
}
/**
* 设置侧边栏模式
*/
function setSidebarMode(mode) {
document.body.classList.remove('sidebar-floating', 'sidebar-docked');
document.body.classList.add('sidebar-' + mode);
console.log('[Sidebar] Mode set to:', mode);
}
/**
* 设置悬浮模式下的鼠标行为
*/
function setupHoverBehavior(wrapper) {
const checkAndSetupHover = () => {
if (document.body.classList.contains('sidebar-floating')) {
wrapper.addEventListener('mouseenter', handleMouseEnter);
wrapper.addEventListener('mouseleave', handleMouseLeave);
} else {
wrapper.removeEventListener('mouseenter', handleMouseEnter);
wrapper.removeEventListener('mouseleave', handleMouseLeave);
}
};
function handleMouseEnter() {
wrapper.classList.add('sidebar-open');
}
function handleMouseLeave() {
wrapper.classList.remove('sidebar-open');
}
checkAndSetupHover();
const observer = new MutationObserver(checkAndSetupHover);
observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
}
initSidebar();
})();

astro.config.mjs 中,将脚本注入到页面头部:

import starlight from '@astrojs/starlight';
import { defineConfig } from 'astro/config';
export default defineConfig({
integrations: [
starlight({
title: 'My Docs',
// 注入自定义脚本
head: [
{
tag: 'script',
attrs: { src: '/scripts/sidebar.js' },
},
],
}),
],
});

astro.config.mjs 的 Starlight 配置中添加自定义 CSS:

starlight({
title: 'My Docs',
// 自定义 CSS 样式
customCss: [
'/src/styles/custom.css',
],
// ... 其他配置
})
  • 悬浮模式(默认):侧边栏收起,只露出 40px 边缘,鼠标悬停时展开
  • 常驻模式:侧边栏固定展开,不自动收起
  • 位置:侧边栏 “Welcome to Starlight” 导航项右侧
  • 图标:☰ 符号
  • 状态:
    • 常驻模式:水平显示 ☰
    • 悬浮模式:旋转 90 度垂直显示 ☰
  • 按钮背景色使用 var(--color-accent)
  • 图标颜色使用 var(--color-sidebarItemText)
  • 自动跟随 Starlight 的深色/浅色主题变化
  • 使用 localStorage 保存用户选择的模式
  • 刷新页面或切换路由后保持用户偏好
  1. 移动端适配:在屏幕宽度小于 768px 时,切换按钮会自动隐藏
  2. 浏览器兼容性:使用现代浏览器特性(CSS 变量、MutationObserver 等)