Make each page refresh feel less Jenky with Smooth Fade Page Transitions

This commit is contained in:
johnnyq
2026-08-27 22:44:53 -04:00
parent 6d5152d1e6
commit f980893cb1
7 changed files with 280 additions and 60 deletions

View File

@@ -1380,3 +1380,122 @@ select.select2[multiple]:not(.tomselected) {
box-shadow: none !important;
}
}
/* ===========================================================================
CROSS-DOCUMENT VIEW TRANSITIONS
===========================================================================
Crossfades between full page loads on same-origin navigation. No JS: the
browser snapshots the outgoing page, loads the incoming one, and animates
between the two snapshots.
Both documents have to carry the at-rule for a navigation to animate, which
is why it lives here - every ITFlow entry point loads this stylesheet, so
agent, admin, client portal, guest, login and setup all opt in together.
Browsers without support ignore the entire block and navigate exactly as
they did before. Nothing below is load-bearing.
Skipped automatically, no handling needed: reloads, address-bar and
bookmark navigations, cross-origin redirects (the M365 OAuth callback, the
Stripe return legs) and anything that never commits a navigation, such as
an authenticated file download. A navigation slower than roughly four
seconds is dropped back to an ordinary page load. */
@view-transition {
navigation: auto;
}
/* Shorter than the 0.25s UA default - this should read as a soft handoff
rather than an animation you sit through. Overriding `animation` alone
keeps the UA stylesheet's mix-blend-mode: plus-lighter, which is what lets
regions that are identical on both pages crossfade without visibly dipping
through a lighter frame in the middle. */
::view-transition-old(root) {
animation: itflow-vt-fade-out 120ms ease both;
}
::view-transition-new(root) {
animation: itflow-vt-fade-in 160ms ease both;
}
@keyframes itflow-vt-fade-out {
to {
opacity: 0;
}
}
@keyframes itflow-vt-fade-in {
from {
opacity: 0;
}
}
/* The header and side nav are deliberately NOT given a view-transition-name.
The first version of this block did, and froze both with animation:none so
they would sit still through the crossfade. That was a mistake. A named
element gets its own ::view-transition-group, and a frozen group holds the
OLD geometry for the whole transition while the incoming snapshot is
stretched to fit it, snapping to its real size only at the end. Geometry
here is not constant the way I assumed: .app-header and .app-sidebar are
fixed to the viewport, and the viewport width changes by the scrollbar width
whenever you navigate between a page long enough to scroll and one that is
not. Hence some navigations looking fine and others not.
Leaving them in the root snapshot is both simpler and better. The UA
stylesheet crossfades ::view-transition-old/new with
mix-blend-mode: plus-lighter precisely so that regions which are identical
on both pages hold steady rather than dipping through a lighter frame in the
middle, and the chrome is identical apart from which nav item carries
.active. Overriding `animation` alone above keeps that blend mode. */
/* Keep the layout viewport a constant width.
This is what was making some navigations judder and others not. AdminLTE
sizes .app-wrapper, .app-header, .app-main and .app-footer against 100vw,
and 100vw INCLUDES the scrollbar, so the usable width is 100vw minus
whatever the scrollbar takes. Nothing reserves that space, so the vertical
scrollbar - and with it the layout width - appears and disappears purely on
whether a given page happens to be taller than the window.
For a view transition that is not cosmetic. ::view-transition-group(root) is
sized to the viewport, and the UA animates the group's width and height from
the outgoing value to the incoming one, stretching BOTH snapshots to fit for
the whole duration and snapping at the end. Navigate between a page that
scrolls and one that does not and every glyph on the page smears sideways by
the scrollbar width and then jumps back.
The client side nav is where this shows worst, because
agent/includes/inc_client_top_head.php renders #clientHeader expanded on
client_overview.php and collapsed everywhere else - so moving between
Overview and any sibling page flips the scrollbar on and off almost every
time.
scrollbar-gutter reserves the space permanently, so the width never moves.
The max-width correction has to come with it: with the gutter always
present, an unqualified 100vw now overruns the layout viewport on EVERY
page rather than only on tall ones, which would trade an intermittent
horizontal jump for a permanent horizontal scrollbar. 100% is the width of
the grid area these four already occupy, which is what the rule wanted.
AdminLTE made the same correction upstream in 4.8.3. */
html {
scrollbar-gutter: stable;
}
.app-wrapper,
.app-header,
.app-main,
.app-footer {
max-width: 100%;
}
/* AdminLTE ships its own reduced-motion handling, but it injects a
#adminlte-reduce-motion style block keyed on *, *::before and *::after.
The ::view-transition pseudo tree is not a descendant of any element those
match, so it is not covered and needs its own opt-out. */
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}