Animate sidebar sub-nav unfurl

Add sidebar sub-nav expand/collapse unfurl, now driven by the CSS View
Transitions API instead of JS tweening. Each nav section is a
view-transition group that clips into a top-to-bottom reveal as it opens
or closes, with a masked gradient soft edge.

A small JS flag (`sidebar-unfurling` on the document element) gates the
gradient to actual open/close, since CSS can't tell a within-section
navigation from a section toggle.
This commit is contained in:
Brandon Oxendine
2026-07-21 17:05:44 +02:00
committed by Rūdolfs Ošiņš
parent 524ff5dfa2
commit b7806bd934
2 changed files with 88 additions and 3 deletions
+64
View File
@@ -672,3 +672,67 @@
::view-transition-old(sidebar-dot):only-child {
animation: sidebar-dot-out 0.2s ease both;
}
/* Marketing sidebar sub-nav unfurl (see Sidebar.svelte). Each nav section is a
view-transition group. As one section grows/shrinks, the sections below tween
their boxes to the new positions (smooth reflow, no jump), and clipping the
growing/shrinking box turns the height change into a top-to-bottom unfurl.
These always run, but on a same-section navigation the boxes don't change
size so it's a no-op. */
::view-transition-group(install-section),
::view-transition-group(learn-section),
::view-transition-group(sidebar-external) {
animation-duration: 0.25s;
animation-timing-function: ease-out;
}
::view-transition-group(install-section),
::view-transition-group(learn-section) {
overflow: clip;
}
/* Anchor the section snapshots to the top at natural height, so a growing/
shrinking group reveals/hides content by clipping rather than scaling it. */
::view-transition-old(install-section),
::view-transition-new(install-section),
::view-transition-old(learn-section),
::view-transition-new(learn-section) {
height: auto;
inset-block-start: 0;
}
/* Animatable soft-edge distance for the reveal, registered so it can tween. */
@property --subnav-fade {
syntax: "<length>";
inherits: false;
initial-value: 0px;
}
@keyframes subnav-fade-edge {
/* Hold the soft edge at full through the 0.25s reveal (0-78% of this 0.32s),
then dissolve it over the short tail so the last item keeps its soft edge
while revealing and the gradient is gone before the pseudo is removed (no
pop, no hard cut). */
0%,
78% {
--subnav-fade: 2rem;
}
100% {
--subnav-fade: 0rem;
}
}
/* The soft-edge gradient only applies when a section actually opens or closes:
the sidebar adds `.sidebar-unfurling` to the document element for exactly
those navigations (see Sidebar.svelte), so moving *within* a section shows no
gradient. `max()` floors the opaque region to the header height so a
collapsing section never fades its own header text. */
:root.sidebar-unfurling::view-transition-image-pair(install-section),
:root.sidebar-unfurling::view-transition-image-pair(learn-section) {
mask-image: linear-gradient(
to bottom,
#000 max(1.75rem, calc(100% - var(--subnav-fade))),
transparent
);
animation: subnav-fade-edge 0.32s ease-out both;
}
+24 -3
View File
@@ -34,6 +34,27 @@
["/learn", "/principles", "/faq", "/glossary"].includes(pathname) ||
pathname.startsWith("/guides");
// Flag the document element only when a section actually opens or closes, so
// the sub-nav unfurl gradient (see marketing.css) fires on expand/collapse
// but not when navigating within an already-open section.
let sidebarStateInitialised = false;
let unfurlTimeout: ReturnType<typeof setTimeout>;
$: flagSectionChange(installOpen, learnOpen);
function flagSectionChange(_installOpen: boolean, _learnOpen: boolean) {
if (!sidebarStateInitialised) {
sidebarStateInitialised = true;
return;
}
if (typeof document === "undefined") {
return;
}
document.documentElement.classList.add("sidebar-unfurling");
clearTimeout(unfurlTimeout);
unfurlTimeout = setTimeout(() => {
document.documentElement.classList.remove("sidebar-unfurling");
}, 500);
}
function onNav(event: MouseEvent, route: Route) {
if (useDefaultNavigation(event)) {
return;
@@ -150,7 +171,7 @@
<aside class="sidebar">
<nav class="top-section">
<div class="nav-section">
<div class="nav-section" style="view-transition-name: install-section;">
<a
href={routeToPath(installRoute)}
class="section-header"
@@ -201,7 +222,7 @@
{/if}
</div>
<div class="nav-section">
<div class="nav-section" style="view-transition-name: learn-section;">
<a
href={routeToPath(learnRoute)}
class="section-header"
@@ -258,7 +279,7 @@
{/if}
</div>
<div class="external-links">
<div class="external-links" style="view-transition-name: sidebar-external;">
<a
href={routeToPath(exploreRoute)}
class="external-link arrow-link"