From 1c5ea2b52eeeaa26324f71ca9406f9da2e7c5f91 Mon Sep 17 00:00:00 2001 From: Vincent van der Wal Date: Thu, 6 Aug 2026 19:24:22 +0200 Subject: [PATCH] beter crossfade --- src/lib/utils/skeleton-fade.ts | 44 ++++ .../weather/14-day/[location]/+page.svelte | 82 ++++---- .../weather/compare/[location]/+page.svelte | 80 ++++---- .../historical/[location]/+page.svelte | 6 +- .../weather/seasonal/[location]/+page.svelte | 74 +++---- .../weather/week/[location]/+page.svelte | 189 +++++++++--------- 6 files changed, 276 insertions(+), 199 deletions(-) create mode 100644 src/lib/utils/skeleton-fade.ts diff --git a/src/lib/utils/skeleton-fade.ts b/src/lib/utils/skeleton-fade.ts new file mode 100644 index 0000000..851aede --- /dev/null +++ b/src/lib/utils/skeleton-fade.ts @@ -0,0 +1,44 @@ +import type { TransitionConfig } from 'svelte/transition'; + +const prefersReducedMotion = () => + typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + +/** + * Fades a loading placeholder out *over* the content that replaces it. + * + * A plain `out:fade` is not usable on a skeleton: Svelte keeps the node in the + * layout for the length of the transition, so the real content stacks below the + * placeholder and the whole page jumps up the moment the placeholder finally + * unmounts. That is why these placeholders only ever faded in - the swap was + * abrupt, but at least nothing moved. + * + * This pins the placeholder to the box it already occupies and takes it out of + * flow for the fade instead. The content settles into its final position + * immediately and the skeleton dissolves on top of it. + * + * Requirements at the call site: the placeholder's parent must be positioned + * (`class="relative"`) and must be the same element that renders the real + * content, or the overlay lands somewhere else on the page. + */ +export function skeletonOut(node: HTMLElement, { duration = 260 } = {}): TransitionConfig { + if (prefersReducedMotion()) return { duration: 0 }; + + // measured while the node is still in flow, which is when Svelte builds the + // transition - the values below are what freeze it in place + const { offsetTop, offsetLeft, offsetWidth, offsetHeight } = node; + + return { + duration, + css: (t) => ` + opacity: ${t}; + position: absolute; + top: ${offsetTop}px; + left: ${offsetLeft}px; + width: ${offsetWidth}px; + height: ${offsetHeight}px; + margin: 0; + pointer-events: none; + z-index: 5; + ` + }; +} diff --git a/src/routes/weather/14-day/[location]/+page.svelte b/src/routes/weather/14-day/[location]/+page.svelte index e440a6f..743053a 100644 --- a/src/routes/weather/14-day/[location]/+page.svelte +++ b/src/routes/weather/14-day/[location]/+page.svelte @@ -1,12 +1,14 @@