Skip to content

Motion

Duration and easing tokens, plus @hyzer-labs/ui/motion: token-bridged transitions, easing evaluators, a scroll-reveal attachment, and a view-transition helper. All of it is reduced-motion-aware by default.

Building your own behavior instead?

The raw IntersectionObserver, ResizeObserver, and MutationObserver attachments live on their own page: Foundation → Observers.

Duration & easing tokens

TokenValue
--hz-duration-fast250ms
--hz-duration-base400ms
--hz-duration-slow550ms
--hz-ease-standardcubic-bezier(0.2, 0, 0, 1)
--hz-ease-incubic-bezier(0.4, 0, 1, 1)
--hz-ease-outcubic-bezier(0, 0, 0.2, 1)

Duration demo

Each bar animates with a different duration token (standard easing).

--hz-duration-fast
--hz-duration-base
--hz-duration-slow

Easing comparison

The same 1100ms duration, three curves. standard is the house default for everything else in the theme (hover/focus state changes). out decelerates into place, and this module picks it for directional entrances (fly, scale). in accelerates away, the mirror for exits.

--hz-ease-standard
--hz-ease-in
--hz-ease-out
--hz-ease-standard plotted as progress over time
standard
--hz-ease-in plotted as progress over time
in
--hz-ease-out plotted as progress over time
out

Transitions

Drop-in replacements for svelte/transition's fade/fly/slide/scale: same params, same return shape. The only change is the import line; duration and easing now default to the tokens above.

Fade
- import { fade } from 'svelte/transition';
+ import { fade } from '@hyzer-labs/ui/motion';

{#if visible}
	<div transition:fade>Content</div>
{/if}

<!-- Same params/semantics as the original. Duration and easing now
     default to the --hz-duration-* / --hz-ease-* tokens, and
     collapse to duration: 0 under prefers-reduced-motion unless
     you pass { essential: true }. -->

A specific instance can opt out of the reduced-motion collapse when the motion itself carries meaning:

import { fly } from '@hyzer-labs/ui/motion';

<!-- A focus-guiding movement carries meaning, so opt it out of the
     reduced-motion collapse rather than losing the cue entirely. -->
<div transition:fly={{ y: -24, essential: true }}>Jumped to top</div>

Scroll reveal

Two attachments, same options. reveal animates one element when it enters the viewport; revealGroup animates a container's direct children, staggered by DOM order. Both hide their targets on the client only, so SSR and no-JS readers always see the content.

Which one you want depends on what should move together: one thing arriving, or several arriving in sequence.

One element, animating on its own the first time it intersects the viewport. This is the common case (a card, an image, a pull-quote) and it takes the same effect / y / threshold options the group form does.

Tomahawk An overhand throw that lands on its edge.
import { reveal } from '@hyzer-labs/ui/motion';

<div class="card" {@attach reveal()}>
	<strong>Tomahawk</strong>
	<span>An overhand throw that lands on its edge.</span>
</div>

View transitions

viewTransition(update) wraps document.startViewTransition. Unsupported browsers and reduced motion run update() directly instead, an instant swap with the same return shape either way. This demo swaps a local layout state (not a page navigation), so it works the same regardless of routing.

Ace
Birdie
Eagle
Bogey
import { viewTransition } from '@hyzer-labs/ui/motion';

function swapLayout() {
	viewTransition(async () => {
		layout = layout === "grid" ? "list" : "grid";
		await tick();
	});
}

<!-- Unsupported browsers and reduced motion run the update directly:
     an instant swap, no error, no branch needed at the call site. -->

Cross-fading real page navigations

For page-to-page transitions, SvelteKit's onNavigate is the integration point. It takes a few lines and does not need this helper, since the DOM update it wraps is the navigation itself:
import { onNavigate } from '$app/navigation';

onNavigate((navigation) => {
	if (!document.startViewTransition) return;
	return new Promise((resolve) => {
		document.startViewTransition(async () => {
			resolve();
			await navigation.complete;
		});
	});
});

The reference theme adds no default ::view-transition-* styling, because the browser's own root cross-fade is already a sensible default. Customizing it is consumer CSS, on the same tokens as everything else:

::view-transition-old(root),
::view-transition-new(root) {
	animation-duration: var(--hz-duration-base);
	animation-timing-function: var(--hz-ease-standard);
}

Reduced motion

Every helper in @hyzer-labs/ui/motion collapses automatically under prefers-reduced-motion: reduce. Transitions run at duration: 0, reveal/revealGroup show their content immediately with no hidden state ever applied, and viewTransition runs its update directly instead of starting a real transition. Pass { essential: true } on any of them to play the full animation anyway. Reserve that for motion which carries meaning, such as a focus-guiding movement, and not for decoration. The state is read fresh on every call, so a preference change mid-session is honored on the very next transition, reveal, or view transition, with no reload needed.

This is the script-side counterpart to the reference theme's own @media (prefers-reduced-motion: reduce) collapse on its CSS transitions. That CSS collapse stays exactly as it is; this module extends the same default to script-driven motion.

Override --hz-duration-* or --hz-ease-* with a plain CSS custom property, or set motion in the hyzer config; see Theming → Tokens & Overrides.

References: Svelte: svelte/transition · MDN: prefers-reduced-motion