<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>响应式固定侧边栏布局 w/ [popover]</title>
<meta name="description" content="使用现代CSS和JavaScript实现的响应式固定侧边栏布局,具有可折叠的树形导航菜单">
<!-- 字体 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Gloria+Hallelujah&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap" rel="stylesheet">
<!-- GSAP 动画库 -->
<script src="https://cdn.skypack.dev/gsap@3.13.0"></script>
<script src="https://cdn.skypack.dev/gsap@3.13.0/Draggable"></script>
<!-- Tweakpane UI控制库 -->
<script src="https://cdn.skypack.dev/tweakpane@4.0.4"></script>
<style>
/* 基础样式和CSS层 */
@import url('https://unpkg.com/normalize.css') layer(normalize);
@layer normalize, base, demo, tree, layout, transitions, mobile, scroller-mask, debug;
@layer {
[data-show="true"] .safety-triangle {
opacity: 1;
}
[data-triangle="false"] .safety-triangle {
pointer-events: none;
background: repeating-linear-gradient(45deg, #0000 0 4px, var(--tree-focus-color) 4px 5px);
opacity: 0.5;
}
[data-show="false"] .safety-triangle {
opacity: 0;
}
}
/* 滚动遮罩层 */
@layer scroller-mask {
@supports(animation-timeline: scroll()) {
sidebar-tree .tree-group-container + .tree-group-container {
--size: 4;
mask-image: linear-gradient(#fff, #0000),
linear-gradient(#fff 0 100%), linear-gradient(#0000, #fff);
mask-size: 100% calc(var(--size) * 1ch), 100% 100%,
100% calc(var(--size) * 1ch);
mask-repeat: no-repeat;
mask-composite: exclude;
animation-timing-function: linear;
animation-timeline: scroll(self);
animation-range: 0 calc(var(--size) * 1ch),
calc(100% - (var(--size) * 1ch)) 100%;
mask-position: 0 0, 0 0, 0 100%;
mask-size: 100% 0, 100% 100%, 100% calc(var(--size) * 1ch);
animation-name: size-up, size-down;
animation-fill-mode: both;
}
}
@keyframes size-up {
to {
mask-size: 100% calc(var(--size) * 1ch), 100% 100%,
100% calc(var(--size) * 1ch);
}
}
@keyframes size-down {
to {
mask-size: 100% calc(var(--size) * 1ch), 100% 100%, 100% 0;
}
}
}
/* 移动端样式 */
@layer mobile {
.mobile-items {
align-items: center;
display: none;
}
.menu-button {
width: 40px;
aspect-ratio: 1;
background: #0000;
border: 0;
display: grid;
place-items: center;
padding: 0;
position: relative;
border-radius: 6px;
cursor: pointer;
}
.menu-button:is(:hover, :focus-visible)::after {
opacity: 1;
}
.menu-button::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
opacity: 0;
background: light-dark(hsl(0 0% 30% / 0.1), hsl(0 0% 90% / .15));
}
aside footer .user {
display: none;
}
aside header .button-link {
display: none;
position: absolute;
top: 0;
left: 0;
color: canvasText;
}
aside header .button-link svg {
translate: -2px 0;
}
.closer {
width: 40px;
aspect-ratio: 1;
position: absolute;
z-index: 2;
top: 0;
right: 0;
background: #0000;
border: 0;
cursor: pointer;
display: grid;
display: none;
place-items: center;
padding: 0;
border-radius: 6px;
}
.closer:is(:hover, :focus-visible)::after {
opacity: 1;
}
.closer::after {
content: '';
position: absolute;
inset: 6px;
border-radius: inherit;
opacity: 0;
background: light-dark(hsl(0 0% 30% / 0.1), hsl(0 0% 90% / .15));
}
.closer svg {
rotate: 45deg;
width: 22px;
border-radius: 6px;
background: #0000;
}
@media(max-width: 768px) {
aside footer .user {
display: flex;
}
aside header .button-link,
.closer {
display: grid;
}
aside footer {
padding: .5rem;
}
aside header {
padding: .5rem;
padding-top: 44px;
}
sidebar-tree [role="treeitem"] {
padding-right: .5rem;
}
kbd {
display: none;
}
header .user,
.user-slash {
display: none;
}
.toggle {
display: none;
}
.mobile-items {
display: flex;
}
aside {
position: fixed;
background: light-dark(#fff, #000);
translate: -100% 0;
height: 100vh;
top: 0;
border-radius: 0;
display: none;
transition-property: display, overlay, translate;
transition-duration: var(--speed);
transition-timing-function: var(--timing);
transition-behavior: allow-discrete;
}
aside::after {
inset: -2px;
}
aside::before,
aside::after {
opacity: 1;
filter: blur(0);
border-radius: 0;
border-top: 0;
border-bottom: 0;
}
aside:popover-open {
display: grid;
translate: 0 0;
}
aside::backdrop {
background: light-dark(hsl(0 0% 0% / 0.6), hsl(0 0% 10% / 0.8));
opacity: 0;
transition-behavior: allow-discrete;
transition-property: display, overlay, opacity;
transition-timing-function: var(--timing);
transition-duration: var(--speed);
}
aside:popover-open::backdrop {
opacity: 1;
}
@starting-style {
aside:popover-open {
translate: -100% 0;
}
aside:popover-open::backdrop {
opacity: 0;
}
}
}
}
/* 过渡动画 */
@layer transitions {
.layout {
display: grid;
grid-template-columns: 0 1fr;
transition: grid-template-columns var(--speed) var(--timing);
}
@media(min-width: 768px) {
.layout:has(:popover-open) {
grid-template-columns: var(--sidebar-width) 1fr;
}
}
.toggle:hover::after {
opacity: 1;
}
.toggle::after {
opacity: 0;
transition: opacity 0.12s ease-out;
}
.toggle .icon-bar {
scale: 0.3 1;
transition: scale var(--speed) var(--timing);
}
.toggle:hover .safety-triangle {
pointer-events: all;
}
.toggle .hover-set,
.toggle .safety-triangle {
transition: scale var(--speed) var(--speed) var(--timing);
opacity: 0;
pointer-events: none;
}
.toggle .hover-set {
pointer-events: all;
}
aside {
translate: -100% 0;
transition-property: translate, height;
transition-duration: var(--speed);
transition-timing-function: var(--timing);
height: calc(100vh - (var(--header-height) + 44px + 1rem));
}
aside::before {
transition: opacity var(--speed) var(--timing);
}
aside::after {
transition-property: opacity, filter;
transition-duration: var(--speed);
transition-timing-function: var(--timing);
}
aside:popover-open {
translate: 0 calc(-44px - 0.25rem);
height: calc(100vh - (var(--header-height) + .5rem - .25rem));
}
.toggle:has(+ :popover-open) .hover-set,
.toggle:has(+ :popover-open) .safety-triangle {
scale: 0;
transition-delay: 0s;
}
.toggle:is(:hover, :focus-visible) .icon-bar,
.toggle:has(+ :popover-open) .icon-bar {
scale: 1 1;
}
.toggle:has(+ :popover-open):is(:hover, :focus-visible) .icon-bar {
scale: 0.3 1;
}
[data-delay=true] {
aside:popover-open sidebar-tree [role="treeitem"],
aside:popover-open header,
aside:popover-open::after,
aside:popover-open::before {
transition-duration: calc(var(--speed) * 1);
}
.toggle:focus-visible:has( + :not(:popover-open)) + aside:before {
transition-delay: 0s;
transition-duration: calc(var(--speed) * 0.25);
}
.toggle:focus-visible + aside:popover-open::before,
aside:popover-open::before {
transition-duration: calc(var(--speed) * 0.25);
transition-delay: calc(var(--speed) * 0.75);
}
.toggle .icon-bar,
.toggle:is(:hover, :focus-visible)::after,
.toggle:has(:is(.safety-triangle:hover, .hover-set:hover)) + aside:not(:popover-open) {
transition-delay: 0.16s;
}
}
.toggle:focus-visible:has(+ :not(:popover-open)) + aside::before {
transition: opacity var(--speed) var(--timing);
}
aside footer,
aside sidebar-tree [role="treeitem"],
aside header {
transition: padding var(--speed) var(--timing);
}
aside:popover-open sidebar-tree [role="treeitem"] {
padding-right: 0.25rem;
}
aside:popover-open header {
padding: .5rem 0 .5rem .5rem;
}
aside:popover-open footer {
padding: .5rem 0 0 .5rem;
}
aside:popover-open::after {
filter: blur(2px);
}
aside:popover-open::after,
aside:popover-open::before {
opacity: 0;
}
@media(min-width: 768px) {
aside:popover-open sidebar-tree .tree-group-container:first-of-type::after,
aside:popover-open footer::after {
right: 0;
}
}
.toggle:has(:is(.safety-triangle:hover, .hover-set:hover)) + aside:not(:popover-open),
.toggle:is(:focus-visible) + aside:not(:popover-open),
aside:is(:focus-within, :hover):not(:popover-open) {
translate: 0 0;
}
}
/* 布局样式 */
@layer layout {
:root {
--tree-focus-color: hsl(0 90% 66%);
--text: light-dark(hsl(0 0% 45%), hsl(0 0% 54%));
--header-height: 60px;
--sidebar-width: 260px;
--panel-color: light-dark(color-mix(in hsl, canvas, canvasText 4%), color-mix(in hsl, #000, canvasText 8%));
--border-color: color-mix(in hsl, canvas, canvasText 30%);
--speed: calc(var(--layout-speed, 0.16) * 1s);
--timing: cubic-bezier(0.0, 0.0, 0.58, 1.0);
}
body {
display: grid;
grid-template-rows: auto 1fr;
padding-bottom: .5rem;
}
.arrow {
opacity: .4;
font-family: Gloria Hallelujah, cursive;
font-size: .875rem;
transition: opacity .26s ease-out;
display: inline-block;
position: absolute;
top: 0;
left: 0;
translate: 60% 130%;
rotate: 14deg;
}
.arrow svg {
width: 60%;
position: absolute;
top: 130%;
left: 0;
translate: -40% 60%;
rotate: 330deg;
scale: -1 1;
}
.arrow span {
display: inline-block;
white-space: nowrap;
}
@media(max-width: 768px) {
.arrow {
display: none;
}
}
aside footer {
padding: .5rem;
display: grid;
gap: .25rem;
width: 100%;
position: relative;
margin-top: .5rem;
}
aside footer::after {
content: '';
position: absolute;
top: 0;
translate: 0 -50%;
left: .5rem;
right: .5rem;
height: 2px;
opacity: .5;
background: var(--border-color);
transition: right var(--speed) var(--timing);
}
.settings, .feedback {
display: flex;
gap: calc(.5rem - 2px);
align-items: center;
height: 32px;
border-radius: 6px;
background: #0000;
border: 0;
color: var(--text-color);
cursor: pointer;
position: relative;
outline-color: var(--tree-focus-color);
}
.settings:is(:hover, :focus-visible),
.feedback:is(:hover, :focus-visible) {
color: canvasText;
}
.settings:is(:hover, :focus-visible)::after,
.feedback:is(:hover, :focus-visible)::after {
opacity: 1;
}
.settings::after,
.feedback::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
background: light-dark(hsl(0 0% 30% / 0.1), hsl(0 0% 90% / .15));
opacity: 0;
}
.settings svg {
width: 28px;
height: 24px;
}
.feedback svg {
height: 22px;
width: 28px;
}
[aria-label="Home"] {
border-radius: 6px;
}
[aria-label="New request"] {
height: 32px;
border-radius: 6px;
border: 0;
cursor: pointer;
color: light-dark(hsl(0 0% 35%), hsl(0 0% 80%));
background: light-dark(hsl(0 0% 86%), hsl(0 0% 24%));
position: relative;
outline-color: var(--tree-focus-color);
}
[aria-label="New request"]:hover {
color: canvasText;
}
[aria-label="New request"]:hover::after {
opacity: 1;
}
[aria-label="New request"]::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
opacity: 0;
background: light-dark(hsl(0 0% 30% / 0.1), hsl(0 0% 90% / .1));
}
.button-link {
aspect-ratio: 1;
display: grid;
place-items: center;
width: 40px;
}
.user {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: .875rem;
height: 32px;
padding: 0 .5rem;
}
.user .avatar {
width: 24px;
aspect-ratio: 1;
background: color-mix(in hsl, canvas, canvasText 25%);
border-radius: 50%;
}
.user .badge {
font-size: .65rem;
padding: .25rem .5rem;
border-radius: 100px;
background: light-dark(hsl(0 0% 80%), hsl(0 0% 30%));
color: light-dark(hsl(0 0% 10%), hsl(0 0% 92%));
}
.header {
height: var(--header-height);
padding-inline: .5rem;
display: flex;
align-items: center;
gap: 0;
}
.header a {
color: inherit;
}
.header svg {
width: 20px;
}
[aria-label="Home"] svg {
translate: -2px 0;
}
.aside {
position: relative;
z-index: 4;
}
.toggle {
position: absolute;
left: calc(100% + 0.5rem);
top: 0rem;
width: 44px;
height: 44px;
display: grid;
place-items: center;
border-radius: 8px;
border-width: 0px;
background: #0000;
cursor: pointer;
position: relative;
outline-color: var(--tree-focus-color);
}
.toggle::after {
content: '';
background: light-dark(hsl(0 0% 25% / 0.25), hsl(0 0% 90% / .25));
opacity: 0;
position: absolute;
inset: 8px;
pointer-events: none;
border-radius: 8px;
}
.toggle .hover-set {
position: absolute;
inset: 0;
border-radius: inherit;
background: repeating-linear-gradient(45deg, #000 0 4px, hsl(140 80% 50%) 4px 5px);
}
.toggle .safety-triangle {
display: inline-block;
width: var(--sidebar-width);
top: 0;
bottom: 0;
background: repeating-linear-gradient(45deg, #0000 0 4px, var(--tree-focus-color) 4px 5px);
position: absolute;
left: 0;
transform-origin: calc(.5rem + 22px) 50%;
translate: -.5rem 0;
clip-path: polygon(0 100%, .5rem 0, calc(.5rem + 44px) 0, 100% 100%);
z-index: 2;
}
.toggle svg {
width: 22px;
}
.toggle svg .icon-bar {
transform-box: fill-box;
transform-origin: 0 50%;
}
.content {
padding-inline: .5rem;
}
aside {
position: fixed;
left: 0;
top: calc(var(--header-height) + 44px);
translate: -100% 0;
overflow: visible;
height: calc(100vh - (var(--header-height) + 44px + 1rem));
}
aside::before {
content: '';
position: absolute;
inset: 0;
border-radius: 0 12px 12px 0;
background: light-dark(#fff, #000);
z-index: -1;
}
aside::after {
content: '';
position: absolute;
inset: -1px;
border: 2px solid var(--border-color);
border-left-color: #0000;
border-radius: 0 12px 12px 0;
pointer-events: none;
}
main {
height: 100%;
border-radius: 8px;
border: 2px solid var(--border-color);
background: var(--panel-color);
position: relative;
}
}
/* 树形导航样式 */
@layer tree {
aside {
background: color-mix(in srgb, canvas, #0000 80%);
border-radius: 8px;
min-width: 250px;
border-radius: 0 12px 12px 0;
margin: 0;
border: 2px solid #0000;
border-left-color: #0000;
height: 100%;
font-size: .875rem;
display: grid;
grid-template-rows: auto 1fr;
color: var(--text);
width: var(--sidebar-width);
padding: 0;
}
aside nav {
overflow: hidden;
}
sidebar-tree {
display: block;
height: 100%;
}
header {
padding: .5rem;
display: grid;
gap: .5rem;
}
header .tree-search-input-container {
position: relative;
}
header .tree-search-input-container svg {
position: absolute;
top: 50%;
left: 8px;
translate: 0 -50%;
width: 16px;
}
header .tree-search-input-container kbd {
border: 1px solid var(--border-color);
position: absolute;
top: 50%;
width: 16px;
height: 16px;
font-size: .625rem;
border-radius: 2px;
color: canvasText;
background: color-mix(in hsl, var(--border-color), #0000);
display: grid;
place-items: center;
right: 8px;
translate: 0 -50%;
}
header h1 {
font-size: .875rem;
margin: 0;
}
header a {
padding-block: .5rem;
padding-left: 2px;
display: flex;
align-items: center;
gap: 0.5rem;
color: light-dark(hsl(0 0% 40%), hsl(0 0% 60%));
text-decoration: none;
outline-color: var(--tree-focus-color);
}
header svg {
width: 16px;
}
.tree-search-input {
width: 100%;
max-width: 100%;
line-height: 2;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: inherit;
background: color-mix(in srgb, canvas 90%, canvasText 10%);
color: canvasText;
padding-left: 26px;
}
.tree-search-input::selection {
background: var(--tree-focus-color);
color: #fff;
}
.tree-search-input:focus-visible {
outline: none;
border-color: var(--tree-focus-color);
}
nav .tree-search-input::placeholder {
color: color-mix(in srgb, canvasText, #0000);
}
sidebar-tree .tree-group-container,
sidebar-tree ul[role="tree"],
sidebar-tree ul[role="group"] {
list-style: none;
margin: 0;
padding: 0;
display: block;
list-style: none;
position: relative;
}
sidebar-tree ul[role=tree] {
display: grid;
height: 100%;
grid-template-rows: auto 1fr;
}
sidebar-tree .tree-group-container:first-of-type {
margin-bottom: .5rem;
padding-bottom: .5rem;
}
sidebar-tree .tree-group-container:first-of-type::after {
content: '';
position: absolute;
top: 100%;
translate: 0 -50%;
left: .5rem;
right: .5rem;
height: 2px;
opacity: .5;
background: var(--border-color);
transition: right var(--speed) var(--timing);
}
sidebar-tree .tree-group-container + .tree-group-container {
height: 100%;
overflow: auto;
scrollbar-color: color-mix(in srgb, var(--tree-focus-color), #0000 50%) transparent;
}
sidebar-tree li > div ul[role="group"] {
margin-left: 1rem;
}
sidebar-tree li > div ul[role="group"] > li:first-of-type {
margin-top: .5rem;
}
sidebar-tree li > div ul[role="group"] > li:last-of-type {
margin-bottom: .5rem;
}
sidebar-tree li > div ul[role="group"]::before {
content: '';
position: absolute;
left: 0;
top: .5rem;
bottom: .5rem;
width: 2px;
background: color-mix(in srgb, currentColor, #0000 65%);
}
sidebar-tree li > div {
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows calc(var(--duration) * 1s) var(--timing);
overflow: hidden;
}
sidebar-tree li > div[inert] {
grid-template-rows: 0fr;
}
[inert] > [role="group"] {
opacity: var(--opacity);
translate: 0 calc(var(--translate) * 1px);
filter: blur(calc(var(--blur) * 1px));
}
[role="group"] {
transition-property: translate, opacity, filter;
transition-duration: calc(var(--duration) * 1s);
transition-timing-function: var(--timing);
translate: 0 0;
filter: blur(0px);
opacity: 1;
}
sidebar-tree li > div > ul {
min-height: 0;
}
sidebar-tree [role="treeitem"] {
display: inline-flex;
align-items: center;
text-decoration: none;
color: inherit;
padding: 0.25rem 0.5rem 0.25rem 1rem;
line-height: 1.6;
width: 100%;
gap: 0.5rem;
position: relative;
}
sidebar-tree [role="treeitem"] span:first-of-type {
flex: 1;
}
sidebar-tree [role="treeitem"]:hover {
color: light-dark(hsl(0 0% 20%), hsl(0 0% 90%));
}
sidebar-tree [role="treeitem"]:focus {
outline: none;
color: light-dark(hsl(0 0% 20%), hsl(0 0% 90%));
background-color: light-dark(
color-mix(in srgb, var(--tree-focus-color), #0000 90%),
color-mix(in srgb, var(--tree-focus-color), #0000 80%)
);
}
sidebar-tree [role="treeitem"][aria-current="page"] {
color: light-dark(#000, #fff);
}
sidebar-tree [role="treeitem"][aria-current="page"]::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: var(--tree-focus-color);
translate: -50% 0;
}
sidebar-tree [role="treeitem"][aria-current="page"][aria-level="1"]::before {
left: 2px;
}
sidebar-tree [role="treeitem"][aria-expanded="true"] .tree-icon svg {
rotate: 135deg;
}
sidebar-tree .tree-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
cursor: pointer;
border-radius: 4px;
}
sidebar-tree .tree-icon:hover {
background-color: color-mix(in srgb, currentColor 10%, transparent);
}
sidebar-tree .tree-icon svg {
width: 16px;
transition: rotate 0.26s ease-out;
}
sidebar-tree ul[role="tree"][data-filtering="true"] [role="treeitem"] {
opacity: 0.6;
}
sidebar-tree ul[role="tree"][data-filtering="true"] [role="treeitem"][data-search-match="true"] {
opacity: 1;
color: light-dark(#000, #fff);
background-color: color-mix(in srgb, var(--tree-focus-color), #0000 60%);
}
sidebar-tree ul[role="tree"][data-filtering="true"] [role="treeitem"][data-search-related="true"] {
color: light-dark(hsl(0 0% 20%), hsl(0 0% 100%));
background-color: color-mix(in srgb, var(--tree-focus-color), #0000 80%);
}
sidebar-tree ul[role="tree"][data-filtering="true"] [role="treeitem"][data-filtered="true"] {
/* 过滤掉的项 */
}
sidebar-tree ul[role="tree"][data-filtering="true"] [role="treeitem"]:focus {
opacity: 1;
}
}
/* 基础样式 */
@layer base {
:root {
--font-size-min: 16;
--font-size-max: 20;
--font-ratio-min: 1.2;
--font-ratio-max: 1.33;
--font-width-min: 375;
--font-width-max: 1500;
}
html {
color-scheme: light dark;
}
[data-theme='light'] {
color-scheme: light only;
}
[data-theme='dark'] {
color-scheme: dark only;
}
:where(.fluid) {
--fluid-min: calc(
var(--font-size-min) * pow(var(--font-ratio-min), var(--font-level, 0))
);
--fluid-max: calc(
var(--font-size-max) * pow(var(--font-ratio-max), var(--font-level, 0))
);
--fluid-preferred: calc(
(var(--fluid-max) - var(--fluid-min)) /
(var(--font-width-max) - var(--font-width-min))
);
--fluid-type: clamp(
(var(--fluid-min) / 16) * 1rem,
((var(--fluid-min) / 16) * 1rem) -
(((var(--fluid-preferred) * var(--font-width-min)) / 16) * 1rem) +
(var(--fluid-preferred) * var(--variable-unit, 100vi)),
(var(--fluid-max) / 16) * 1rem
);
font-size: var(--fluid-type);
}
*,
*:after,
*:before {
box-sizing: border-box;
}
body {
background: light-dark(#fff, #000);
min-height: 100vh;
font-family: 'Inter', 'SF Pro Text', 'SF Pro Icons', 'AOS Icons', 'Helvetica Neue',
Helvetica, Arial, sans-serif, system-ui;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
}
/* Tweakpane 面板样式 */
div.tp-dfwv {
top: 18px;
width: 256px;
position: fixed;
}
* {
-webkit-tap-highlight-color: #0000;
}
</style>
</head>
<body>
<header class="header">
<div class="mobile-items">
<button class="menu-button" popovertarget="sidebar" popovertargetaction="toggle" aria-label="Open navigation">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
</button>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m9 20.247 6-16.5" />
</svg>
</div>
<a class="button-link" aria-label="Home" href="/">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m3.98448 20.5882-1.17645 1.6174m-.22047-1.3969 1.61739 1.1764M22.144.730378 20.6832 2.09646m.0474-1.413417 1.366 1.460757" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="m3.90821 18.5526-.59559.0726c.03203.2626.23224.4732.49285.5185l.10274-.5911ZM20.601 4.22249l.3908.45525c.1679-.14415.2428-.36902.195-.58508-.0479-.21606-.2109-.38822-.424-.44793l-.1618.57776Zm.6023-.00906c-.0066-.33131-.2805-.59453-.6118-.58794-.3313.0066-.5945.28052-.5879.61183l1.1997-.02389Zm-.2556 17.30047-.1028.5911c.1766.0307.3576-.0191.4935-.1358.136-.1168.2127-.2881.2091-.4673l-.5998.012ZM3.03505 11.3945l-.39082-.4552c-.15222.1306-.22906.3287-.20477.5279l.59559-.0727ZM13.8216 1.69999c-.3191-.0894-.6502.0968-.7396.41588-.0894.31909.0968.65023.4159.73963l.3237-1.15551ZM4.29903 19.0078 20.9918 4.67774l-.7816-.91051L3.51739 18.0973l.78164.9105ZM20.0036 4.23732l.3442 17.28848 1.1997-.0239-.3442-17.28847-1.1997.02389ZM3.80547 19.1437 20.8449 22.105l.2055-1.1823-17.03946-2.9613-.20547 1.1823Zm-.3796-7.2939L14.0485 2.73064l-.7816-.91052L2.64423 10.9393l.78164.9105Zm1.07792 6.6301-.87316-7.158-1.19117.1453.87316 7.158 1.19117-.1453ZM20.7628 3.64473l-6.9412-1.94474-.3237 1.15551 6.9412 1.94474.3237-1.15551Z" fill="currentColor"></path>
<path d="m15.7227 7.75 4.9996 13.5001M8.13672 14.2656 20.724 21.2521" stroke="currentColor" stroke-width="1.2"></path>
<path d="M7.0673 8.29748c-.11208-.31184-.45574-.47378-.76758-.36169-.31184.11208-.47377.45574-.36169.76758l1.12927-.40589Zm2.05515 5.71782L7.0673 8.29748l-1.12927.40589 2.05515 5.71783 1.12927-.4059Z" fill="currentColor"></path>
<path d="m15.6133 8.16406-5.1156-2.66251" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"></path>
</svg>
</a>
<svg class="user-slash" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m9 20.247 6-16.5" />
</svg>
<div class="user">
<img class="avatar" alt="" src="https://assets.codepen.io/605876/cropped-headshot--saturated-low-res.jpg"/>
<span class="name">Jhey Tompkins</span>
<span class="badge">Lifetime</span>
</div>
</header>
<div class="layout">
<div class="aside">
<button popovertarget="sidebar" popovertargetaction="toggle" class="toggle">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 8.25V18C3 18.5967 3.23705 19.169 3.65901 19.591C4.08097 20.0129 4.65326 20.25 5.25 20.25H18.75C19.3467 20.25 19.919 20.0129 20.341 19.591C20.7629 19.169 21 18.5967 21 18V8.25M3 8.25V6C3 5.40326 3.23705 4.83097 3.65901 4.40901C4.08097 3.98705 4.65326 3.75 5.25 3.75H18.75C19.3467 3.75 19.919 3.98705 20.341 4.40901C20.7629 4.83097 21 5.40326 21 6V8.25M3 8.25H21M5.25 6H5.258V6.008H5.25V6ZM7.5 6H7.508V6.008H7.5V6ZM9.75 6H9.758V6.008H9.75V6Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path class="icon-bar" d="M4.75 10H11V18.5H5.75C5.19772 18.5 4.75 18.0523 4.75 17.5V10Z" fill="currentColor"/>
</svg>
<span class="safety-triangle"></span>
<span class="hover-set"></span>
</button>
<aside popover="manual" id="sidebar">
<header>
<a class="button-link" aria-label="Home" href="/">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m3.98448 20.5882-1.17645 1.6174m-.22047-1.3969 1.61739 1.1764M22.144.730378 20.6832 2.09646m.0474-1.413417 1.366 1.460757" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="m3.90821 18.5526-.59559.0726c.03203.2626.23224.4732.49285.5185l.10274-.5911ZM20.601 4.22249l.3908.45525c.1679-.14415.2428-.36902.195-.58508-.0479-.21606-.2109-.38822-.424-.44793l-.1618.57776Zm.6023-.00906c-.0066-.33131-.2805-.59453-.6118-.58794-.3313.0066-.5945.28052-.5879.61183l1.1997-.02389Zm-.2556 17.30047-.1028.5911c.1766.0307.3576-.0191.4935-.1358.136-.1168.2127-.2881.2091-.4673l-.5998.012ZM3.03505 11.3945l-.39082-.4552c-.15222.1306-.22906.3287-.20477.5279l.59559-.0727ZM13.8216 1.69999c-.3191-.0894-.6502.0968-.7396.41588-.0894.31909.0968.65023.4159.73963l.3237-1.15551ZM4.29903 19.0078 20.9918 4.67774l-.7816-.91051L3.51739 18.0973l.78164.9105ZM20.0036 4.23732l.3442 17.28848 1.1997-.0239-.3442-17.28847-1.1997.02389ZM3.80547 19.1437 20.8449 22.105l.2055-1.1823-17.03946-2.9613-.20547 1.1823Zm-.3796-7.2939L14.0485 2.73064l-.7816-.91052L2.64423 10.9393l.78164.9105Zm1.07792 6.6301-.87316-7.158-1.19117.1453.87316 7.158 1.19117-.1453ZM20.7628 3.64473l-6.9412-1.94474-.3237 1.15551 6.9412 1.94474.3237-1.15551Z" fill="currentColor"></path>
<path d="m15.7227 7.75 4.9996 13.5001M8.13672 14.2656 20.724 21.2521" stroke="currentColor" stroke-width="1.2"></path>
<path d="M7.0673 8.29748c-.11208-.31184-.45574-.47378-.76758-.36169-.31184.11208-.47377.45574-.36169.76758l1.12927-.40589Zm2.05515 5.71782L7.0673 8.29748l-1.12927.40589 2.05515 5.71783 1.12927-.4059Z" fill="currentColor"></path>
<path d="m15.6133 8.16406-5.1156-2.66251" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"></path>
</svg>
</a>
<button class="closer" popovertarget="sidebar" popovertargetaction="hide" aria-label="Close menu">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
</button>
<button aria-label="New request">New Request</button>
<form>
<div class="tree-search-input-container">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11ZM2 9a7 7 0 1 1 12.452 4.391l3.328 3.329a.75.75 0 1 1-1.06 1.06l-3.329-3.328A7 7 0 0 1 2 9Z" clip-rule="evenodd" />
</svg>
<input
type="input"
id="tree-search"
placeholder="Find"
aria-label="Filter navigation tree"
class="tree-search-input"
>
<kbd>/</kbd>
</div>
</form>
</header>
<nav aria-label="The Craft of UI">
<sidebar-tree>
<!-- Tree content will be generated by the component -->
</sidebar-tree>
</nav>
<footer>
<div class="user">
<img class="avatar" alt="" src="https://assets.codepen.io/605876/cropped-headshot--saturated-low-res.jpg"/>
<span class="name">Jhey Tompkins</span>
<span class="badge">Lifetime</span>
</div>
<button class="feedback" aria-label="Feedback">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
<span>Feedback</span>
</button>
<button class="settings" aria-label="Settings">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12a7.5 7.5 0 0 0 15 0m-15 0a7.5 7.5 0 1 1 15 0m-15 0H3m16.5 0H21m-1.5 0H12m-8.457 3.077 1.41-.513m14.095-5.13 1.41-.513M5.106 17.785l1.15-.964m11.49-9.642 1.149-.964M7.501 19.795l.75-1.3m7.5-12.99.75-1.3m-6.063 16.658.26-1.477m2.605-14.772.26-1.477m0 17.726-.26-1.477M10.698 4.614l-.26-1.477M16.5 19.794l-.75-1.299M7.5 4.205 12 12m6.894 5.785-1.149-.964M6.256 7.178l-1.15-.964m15.352 8.864-1.41-.513M4.954 9.435l-1.41-.514M12.002 12l-3.75 6.495" />
</svg>
<span>Settings</span>
</button>
</footer>
</aside>
</div>
<div class="content">
<main>
<span class="arrow arrow--instruction">
<span>mess with this</span>
<svg viewBox="0 0 97 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M74.568 0.553803C74.0753 0.881909 73.6295 1.4678 73.3713 2.12401C73.1367 2.70991 72.3858 4.67856 71.6584 6.50658C70.9544 8.35803 69.4526 11.8031 68.3498 14.1936C66.1441 19.0214 65.839 20.2167 66.543 21.576C67.4581 23.3337 69.4527 23.9196 71.3064 22.9821C72.4797 22.3728 74.8965 19.5839 76.9615 16.4435C78.8387 13.5843 78.8387 13.6077 78.1113 18.3418C77.3369 23.4275 76.4687 26.2866 74.5915 30.0364C73.254 32.7316 71.8461 34.6299 69.218 37.3485C65.9563 40.6999 62.2254 42.9732 57.4385 44.4965C53.8718 45.6449 52.3935 45.8324 47.2546 45.8324C43.3594 45.8324 42.1158 45.7386 39.9805 45.2933C32.2604 43.7466 25.3382 40.9577 19.4015 36.9735C15.0839 34.0909 12.5028 31.7004 9.80427 27.9975C6.80073 23.9196 4.36038 17.2403 3.72682 11.475C3.37485 8.1471 3.1402 7.32683 2.43624 7.13934C0.770217 6.71749 0.183578 7.77211 0.0193217 11.5219C-0.26226 18.5996 2.55356 27.1304 7.17619 33.1066C13.8403 41.7545 25.432 48.4103 38.901 51.2696C41.6465 51.8555 42.2566 51.9023 47.4893 51.9023C52.3935 51.9023 53.426 51.832 55.5144 51.3867C62.2723 49.9337 68.5375 46.6292 72.949 42.1998C76.0464 39.1296 78.1113 36.2939 79.8946 32.7081C82.1942 28.0912 83.5317 23.3103 84.2591 17.17C84.3999 15.8576 84.6111 14.7795 84.7284 14.7795C84.8223 14.7795 85.4559 15.1311 86.1364 15.5763C88.037 16.7716 90.3835 17.8965 93.5748 19.0918C96.813 20.3339 97.3996 20.287 96.4141 18.9512C94.9123 16.9122 90.055 11.5219 87.1219 8.63926C84.0949 5.66288 83.8368 5.33477 83.5552 4.1864C83.3909 3.48332 83.0155 2.68649 82.6401 2.31151C82.0065 1.6553 80.4109 1.04595 79.9885 1.30375C79.8712 1.37406 79.2845 1.11626 78.6744 0.717845C77.2431 -0.172727 75.7413 -0.243024 74.568 0.553803Z" fill="currentColor"></path>
</svg>
</span>
</main>
</div>
</div>
<script>
// 注册GSAP插件
gsap.registerPlugin(Draggable);
const config = {
theme: 'system',
duration: 0.18,
opacity: 0.4,
blur: 10,
translate: 12,
speed: 0.16,
delay: true,
triangle: true,
show: false,
};
// 创建Tweakpane控制面板
const ctrl = new Pane({
title: 'config',
expanded: false,
});
const update = () => {
document.documentElement.dataset.theme = config.theme;
document.documentElement.dataset.delay = config.delay;
document.documentElement.dataset.triangle = config.triangle;
document.documentElement.dataset.show = config.show;
document.documentElement.style.setProperty('--layout-speed', config.speed);
document.documentElement.style.setProperty('--duration', config.duration);
document.documentElement.style.setProperty('--opacity', config.opacity);
document.documentElement.style.setProperty('--blur', config.blur);
document.documentElement.style.setProperty('--translate', config.translate);
};
const sync = (event) => {
if (
!document.startViewTransition ||
event.target.controller.view.labelElement.innerText !== 'theme'
)
return update();
document.startViewTransition(() => update());
};
const layoutConfig = ctrl.addFolder({ title: 'layout', expanded: false });
layoutConfig.addBinding(config, 'speed', {
min: 0,
max: 1,
speed: 0.01,
label: 'speed(s)'
});
layoutConfig.addBinding(config, 'delay');
layoutConfig.addBinding(config, 'show');
layoutConfig.addBinding(config, 'triangle');
const sidebarConfig = ctrl.addFolder({ title: 'sidebar', expanded: false });
sidebarConfig.addBinding(config, 'duration', {
min: 0.1,
max: 2,
step: 0.01,
label: 'duration(s)',
});
sidebarConfig.addBinding(config, 'opacity', {
min: 0,
max: 1,
step: 0.1,
});
sidebarConfig.addBinding(config, 'blur', {
min: 0,
max: 20,
step: 1,
label: 'blur(px)',
});
sidebarConfig.addBinding(config, 'translate', {
min: 0,
max: 40,
step: 1,
label: 'translate(px)',
});
ctrl.addBinding(config, 'theme', {
label: 'theme',
options: {
system: 'system',
light: 'light',
dark: 'dark',
},
});
ctrl.on('change', sync);
update();
// 使Tweakpane面板可拖动
const tweakClass = 'div.tp-dfwv';
const d = Draggable.create(tweakClass, {
type: 'x,y',
allowEventDefault: true,
trigger: tweakClass + ' button.tp-rotv_b',
});
document.querySelector(tweakClass).addEventListener('dblclick', () => {
gsap.to(tweakClass, {
x: `+=${d[0].x * -1}`,
y: `+=${d[0].y * -1}`,
onComplete: () => {
gsap.set(tweakClass, { clearProps: 'all' });
},
});
});
// 树形导航数据结构
const TREE_DATA = {
label: "Mythical University",
groups: [
{
title: "Base",
items: [
{
id: "intro",
label: "Introduction",
href: "#introduction",
current: true
},
{
id: "getting-started",
label: "Getting Started",
href: "#getting-started",
current: false
},
{
id: "checklist",
label: "The Checklist",
href: "#checklist",
},
{
id: "requests",
label: "Requests",
href: "#requests",
}
]
},
{
title: "Modules",
items: [
{
id: "foundations",
label: "Foundations",
href: "#foundations",
items: [
{
id: "overview",
label: "Overview",
href: "#overview"
},
{
id: "css-animation",
label: "CSS Animation",
href: "#css-animation",
items: [
{
id: "css-animation-anatomy",
label: "Anatomy",
href: "#css-animation-anatomy"
},
{
id: "first-keyframe",
label: "Keyframes",
href: "#keyframes"
},
{
id: "delays",
label: "Delays",
href: "#delays"
},
]
},
{
id: "svg-filters",
label: "SVG Filters",
href: "#svg-filters",
items: [
{
id: "svg-filter-anatomy",
label: "Anatomy",
href: "#svg-filter-anatomy"
},
{
id: "goo",
label: "Goo",
href: "#goo"
},
{
id: "noise",
label: "Noise",
href: "#noise"
}
]
},
{
id: "canvas",
label: "Canvas",
href: "#canvas",
items: [
{
id: "canvas-anatomy",
label: "Anatomy",
href: "#canvas-anatomy"
},
{
id: "particles",
label: "Particles",
href: "#particles"
},
{
id: "projection",
label: "Projection",
href: "#projection"
}
]
}
]
},
{
id: "studio",
label: "Studio",
href: "#studio",
items: [
{
id: "tri-toggle",
label: "Tri-Toggle",
href: "#tri-toggle"
},
{
id: "liquid-glass",
label: "Liquid Glass",
href: "#liquid-glass",
items: [
{
id: "liquid-displacement",
label: "Displacement",
href: "#liquid-displacement"
},
{
id: "liquid-toggle",
label: "Toggle",
href: "#liquid-toggle"
},
{
id: "liquid-slider",
label: "Slider",
href: "#liquid-slider"
}
]
},
{
id: "bear-toggle",
label: "Bear toggle",
href: "#bear-toggle"
},
{
id: "you-can-scroll",
label: "You can scroll",
href: "#you-can-scroll"
},
{
id: "split-flap-display",
label: "3D Split Flap",
href: "#split-flap-display"
},
{
id: "signature-flow",
label: "Signature flow",
href: "#signature-flow"
}
]
},
{
id: "horizon",
label: "Horizon",
href: "#horizon",
items: [
{
id: "scroll-markers",
label: ":scroll-marker-group",
href: "#scroll-markers"
},
{
id: "css-scroll-animation",
label: "Scroll-driven Animation",
href: "#css-scroll-animation"
},
{
id: "starting-style",
label: "@starting-style",
href: "#starting-style"
},
{
id: "details-content",
label: "::details-content",
href: "#details-content"
},
{
id: "styleable-select",
label: "Styleable Select",
href: "#styleable-select"
},
{
id: "view-transitions",
label: "View Transitions",
href: "#view-transitions"
},
{
id: "scroll-target-group",
label: "scroll-target-group",
href: "#scroll-target-group"
},
{
id: "stuck",
label: ":stuck",
href: "#stuck"
}
]
}
]
}
]
};
// 生成树形导航HTML
function generateTreeHTML(data) {
const processItems = (items, level = 1, parentId = null) => {
const setSize = items.length;
const htmlParts = [];
items.forEach((item, index) => {
const posInSet = index + 1;
const hasChildren = item.items && item.items.length > 0;
const itemId = `tree-item-${item.id}`;
const groupId = hasChildren ? `tree-group-${item.id}` : null;
let html = `<li role="none">`;
html += `<a
id="${itemId}"
role="treeitem"
href="${item.href || '#'}"
tabindex="${item.current ? '0' : '-1'}"
aria-level="${level}"
aria-setsize="${setSize}"
aria-posinset="${posInSet}"
${item.current ? 'aria-current="page"' : ''}
${hasChildren ? `aria-expanded="false" aria-owns="${groupId}"` : ''}
>`;
html += `<span>${item.label}</span>`;
if (hasChildren) {
html += `<span class="tree-icon" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
</span>`;
}
html += `</a>`;
if (hasChildren) {
html += `<div inert>`;
html += `<ul id="${groupId}" role="group">`;
html += processItems(item.items, level + 1, itemId);
html += `</ul>`;
html += `</div>`;
}
html += `</li>`;
htmlParts.push(html);
});
return htmlParts.join('');
};
if (data.groups) {
let treeHTML = `<ul role="tree" aria-label="${data.label}">`;
data.groups.forEach((group, groupIndex) => {
const groupId = `tree-group-toplevel-${groupIndex}`;
treeHTML += `<li role="none" class="tree-group-container">`;
treeHTML += `<ul role="group" id="${groupId}">`;
treeHTML += processItems(group.items);
treeHTML += `</ul>`;
treeHTML += `</li>`;
});
treeHTML += `</ul>`;
return treeHTML;
}
return `
<ul role="tree" aria-label="${data.label}">
${processItems(data.items || [])}
</ul>
`;
}
document.querySelector('sidebar-tree').innerHTML = generateTreeHTML(TREE_DATA);
// 侧边栏树形导航组件
class SidebarTree extends HTMLElement {
constructor() {
super();
this.currentFocus = null;
this.nodeMap = new Map();
}
resetTabIndexes() {
this.tree.querySelectorAll('[role="treeitem"]').forEach(el => {
el.setAttribute('tabindex', '-1');
});
}
setFocusToItem(item, updateTabindex = true) {
if (!item) return;
if (updateTabindex) {
this.resetTabIndexes();
item.setAttribute('tabindex', '0');
}
item.focus();
this.currentFocus = item;
}
isExpanded(item) {
return item.getAttribute('aria-expanded') === 'true';
}
findParentTreeItem(childElement) {
const parentGroup = childElement.closest('ul[role="group"][id]');
if (parentGroup && parentGroup.id.startsWith('tree-group-') && !parentGroup.id.includes('toplevel')) {
return this.querySelector(`[aria-owns="${parentGroup.id}"]`);
}
return null;
}
getGroupFromItem(item) {
const groupId = item.getAttribute('aria-owns');
return groupId ? document.getElementById(groupId) : null;
}
connectedCallback() {
this.tree = this.querySelector('[role="tree"]');
this.buildNodeMap();
this.setupEventListeners();
this.initializeFocus();
}
buildNodeMap() {
const allTreeItems = this.querySelectorAll('[role="treeitem"]');
allTreeItems.forEach(item => {
const parentItem = this.findParentTreeItem(item);
this.nodeMap.set(item.id, {
id: item.id,
level: parseInt(item.getAttribute('aria-level')),
hasChildren: item.hasAttribute('aria-expanded'),
parentId: parentItem?.id || null,
label: item.textContent.trim()
});
});
}
setupEventListeners() {
this.tree.addEventListener('click', this.handleClick.bind(this));
this.tree.addEventListener('keydown', this.handleKeydown.bind(this));
}
initializeFocus() {
const currentItem = this.tree.querySelector('[aria-current="page"]');
this.currentFocus = currentItem || this.tree.querySelector('[role="treeitem"]');
if (currentItem) {
this.ensureItemVisible(currentItem);
}
}
handleClick(event) {
const treeItem = event.target.closest('[role="treeitem"]');
if (!treeItem) return;
const icon = event.target.closest('.tree-icon');
if (icon && treeItem.hasAttribute('aria-expanded')) {
event.preventDefault();
this.toggleExpanded(treeItem);
} else if (!icon) {
this.activateItem(treeItem);
}
}
handleKeydown(event) {
const treeItem = event.target.closest('[role="treeitem"]');
if (!treeItem) return;
switch (event.key) {
case 'Enter':
case ' ':
event.preventDefault();
this.activateItem(treeItem);
treeItem.click();
break;
case 'ArrowDown':
event.preventDefault();
this.focusNextItem(treeItem);
break;
case 'ArrowUp':
event.preventDefault();
this.focusPreviousItem(treeItem);
break;
case 'ArrowRight':
event.preventDefault();
this.handleRightArrow(treeItem);
break;
case 'ArrowLeft':
event.preventDefault();
this.handleLeftArrow(treeItem);
break;
case 'Home':
event.preventDefault();
this.focusFirstItem();
break;
case 'End':
event.preventDefault();
this.focusLastItem();
break;
case '*':
event.preventDefault();
this.expandAllSiblings(treeItem);
break;
default:
if (event.key.length === 1 && event.key.match(/[a-zA-Z]/)) {
event.preventDefault();
this.focusItemByFirstChar(event.key.toLowerCase());
}
}
}
toggleExpanded(item) {
const wasExpanded = this.isExpanded(item);
const group = this.getGroupFromItem(item);
if (group) {
const wrapper = group.parentElement;
item.setAttribute('aria-expanded', !wasExpanded);
if (wasExpanded) {
wrapper.setAttribute('inert', '');
} else {
wrapper.removeAttribute('inert');
}
}
}
activateItem(item) {
this.tree.querySelectorAll('[aria-current="page"]').forEach(el => {
el.removeAttribute('aria-current');
});
item.setAttribute('aria-current', 'page');
this.resetTabIndexes();
item.setAttribute('tabindex', '0');
}
focusItem(item) {
this.setFocusToItem(item);
}
focusNextItem(current) {
const allVisible = this.getVisibleItems();
const currentIndex = allVisible.indexOf(current);
if (currentIndex < allVisible.length - 1) {
this.focusItem(allVisible[currentIndex + 1]);
}
}
focusPreviousItem(current) {
const allVisible = this.getVisibleItems();
const currentIndex = allVisible.indexOf(current);
if (currentIndex > 0) {
this.focusItem(allVisible[currentIndex - 1]);
}
}
handleRightArrow(item) {
if (item.hasAttribute('aria-expanded')) {
if (!this.isExpanded(item)) {
this.toggleExpanded(item);
} else {
const group = this.getGroupFromItem(item);
const firstChild = group?.querySelector('[role="treeitem"]');
if (firstChild) {
this.focusItem(firstChild);
}
}
}
}
handleLeftArrow(item) {
const nodeInfo = this.nodeMap.get(item.id);
if (item.hasAttribute('aria-expanded') && this.isExpanded(item)) {
this.toggleExpanded(item);
} else if (nodeInfo.parentId) {
const parent = document.getElementById(nodeInfo.parentId);
if (parent) {
this.focusItem(parent);
}
}
}
focusFirstItem() {
const firstItem = this.tree.querySelector('[role="treeitem"]');
this.focusItem(firstItem);
}
focusLastItem() {
const allVisible = this.getVisibleItems();
this.focusItem(allVisible[allVisible.length - 1]);
}
expandAllSiblings(item) {
const nodeInfo = this.nodeMap.get(item.id);
const parent = nodeInfo.parentId ?
document.getElementById(nodeInfo.parentId).parentElement :
this.tree;
parent.querySelectorAll(':scope > li > [aria-expanded="false"]').forEach(sibling => {
this.toggleExpanded(sibling);
});
}
focusItemByFirstChar(char) {
const allVisible = this.getVisibleItems();
const current = document.activeElement;
const currentIndex = allVisible.indexOf(current);
for (let i = currentIndex + 1; i < allVisible.length; i++) {
if (allVisible[i].textContent.toLowerCase().trim().startsWith(char)) {
this.focusItem(allVisible[i]);
return;
}
}
for (let i = 0; i <= currentIndex; i++) {
if (allVisible[i].textContent.toLowerCase().trim().startsWith(char)) {
this.focusItem(allVisible[i]);
return;
}
}
}
getVisibleItems() {
const items = [];
const walkTree = (element) => {
const directItems = element.querySelectorAll(':scope > li > [role="treeitem"]');
const groupItems = element.querySelectorAll(':scope > li > ul[role="group"] > li > [role="treeitem"]');
const treeItems = [...directItems, ...groupItems];
treeItems.forEach(item => {
items.push(item);
if (this.isExpanded(item)) {
const group = this.getGroupFromItem(item);
if (group) {
walkTree(group);
}
}
});
};
walkTree(this.tree);
return items;
}
ensureItemVisible(item) {
let parent = item.parentElement;
while (parent && parent !== this.tree) {
if (parent.getAttribute('role') === 'group') {
const wrapper = parent.parentElement;
if (wrapper && wrapper.hasAttribute('inert')) {
const parentItem = this.tree.querySelector(`[aria-owns="${parent.id}"]`);
if (parentItem && !this.isExpanded(parentItem)) {
this.toggleExpanded(parentItem);
}
}
}
parent = parent.parentElement;
}
}
filter(searchTerm) {
const allItems = this.tree.querySelectorAll('[role="treeitem"]');
if (!searchTerm || searchTerm.length < 3) {
allItems.forEach(item => {
item.removeAttribute('data-filtered');
item.removeAttribute('data-search-match');
item.removeAttribute('data-search-related');
});
this.tree.removeAttribute('data-filtering');
const allExpandable = this.tree.querySelectorAll('[aria-expanded="true"]');
allExpandable.forEach(item => {
this.toggleExpanded(item);
});
const currentItem = this.tree.querySelector('[aria-current="page"]');
if (currentItem) {
this.ensureItemVisible(currentItem);
}
return 0;
}
this.tree.setAttribute('data-filtering', 'true');
const term = searchTerm.toLowerCase();
const matches = new Set();
const relatedItems = new Set();
allItems.forEach(item => {
const text = item.textContent.toLowerCase();
if (text.includes(term)) {
matches.add(item);
item.setAttribute('data-search-match', 'true');
let parent = item.parentElement;
while (parent && parent !== this.tree) {
if (parent.getAttribute('role') === 'group') {
const parentItem = this.tree.querySelector(`[aria-owns="${parent.id}"]`);
if (parentItem) {
relatedItems.add(parentItem);
if (!this.isExpanded(parentItem)) {
this.toggleExpanded(parentItem);
}
}
}
parent = parent.parentElement;
}
if (item.hasAttribute('aria-owns')) {
const group = this.getGroupFromItem(item);
if (group) {
const descendants = group.querySelectorAll('[role="treeitem"]');
descendants.forEach(desc => relatedItems.add(desc));
if (!this.isExpanded(item)) {
this.toggleExpanded(item);
}
}
}
}
});
allItems.forEach(item => {
if (matches.has(item)) {
item.removeAttribute('data-filtered');
item.removeAttribute('data-search-related');
} else if (relatedItems.has(item)) {
item.removeAttribute('data-filtered');
item.removeAttribute('data-search-match');
item.setAttribute('data-search-related', 'true');
} else {
item.removeAttribute('data-search-match');
item.removeAttribute('data-search-related');
item.setAttribute('data-filtered', 'true');
}
});
return matches.size;
}
}
customElements.define('sidebar-tree', SidebarTree);
// 搜索功能
const searchInput = document.getElementById('tree-search');
const sidebarTree = document.querySelector('sidebar-tree');
function updateSearchAriaLabel(value, matches) {
const baseLabel = 'Search navigation tree - Press slash to focus';
if (!value || value.length < 3) {
searchInput.setAttribute('aria-label', baseLabel);
} else {
searchInput.setAttribute('aria-label',
matches > 0
? `Search navigation tree - ${matches} items found - Press slash to focus`
: 'Search navigation tree - No items found - Press slash to focus'
);
}
}
if (searchInput && sidebarTree) {
searchInput.addEventListener('input', (e) => {
const value = e.target.value.trim();
const matches = sidebarTree.filter(value);
updateSearchAriaLabel(value, matches);
});
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
e.target.value = '';
sidebarTree.filter('');
updateSearchAriaLabel('', 0);
}
});
document.addEventListener('keydown', (e) => {
const tagName = e.target.tagName.toLowerCase();
const isEditable = e.target.isContentEditable;
const isInput = tagName === 'input' || tagName === 'textarea' || tagName === 'select';
if (e.key === '/' && !isInput && !isEditable) {
e.preventDefault();
searchInput.focus();
searchInput.select();
}
});
}
// 响应式popover处理
const sidebar = document.querySelector('aside[popover]');
const syncPopover = () => {
const desktop = window.matchMedia('(min-width: 768px)').matches;
sidebar.setAttribute('popover', desktop ? 'manual' : 'auto');
};
window.addEventListener('resize', syncPopover);
syncPopover();
</script>
</body>
</html>
index.html
style.css
index.js
index.html