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?
IntersectionObserver, ResizeObserver, and MutationObserver attachments live on their own page: Foundation → Observers.Duration & easing tokens
| Token | Value |
|---|---|
--hz-duration-fast | 250ms |
--hz-duration-base | 400ms |
--hz-duration-slow | 550ms |
--hz-ease-standard | cubic-bezier(0.2, 0, 0, 1) |
--hz-ease-in | cubic-bezier(0.4, 0, 1, 1) |
--hz-ease-out | cubic-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 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.
- 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 }. -->- import { fly } from 'svelte/transition';
+ import { fly } from '@hyzer-labs/ui/motion';
{#if visible}
<div transition:fly>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 }. -->- import { slide } from 'svelte/transition';
+ import { slide } from '@hyzer-labs/ui/motion';
{#if visible}
<div transition:slide>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 }. -->- import { scale } from 'svelte/transition';
+ import { scale } from '@hyzer-labs/ui/motion';
{#if visible}
<div transition:scale>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.
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>effect picks the animation style for the whole group and mirrors the
transition family above 1:1: fade, fly (the default), slide, scale. stagger offsets each child by DOM
order. fly steers by its x/y offsets: positive y rises from below (the default, y: 16), negative y drops from above, and x travels in from the side. slide expands from the center line of axis without moving
the box, and scale grows from start.
import { revealGroup } from '@hyzer-labs/ui/motion';
<div class="strip" {@attach revealGroup({ stagger: 160 })}>
{#each cards as card}
<div class="card">{card}</div>
{/each}
</div>A real Hero in overlay layout, with only the content revealed:
the background artwork is already there when the copy arrives. Each part animates separately,
so Stagger drives the delay between them rather than a group option.
Throw straighter, sooner
import { Hero, Button } from '@hyzer-labs/ui';
import { reveal } from '@hyzer-labs/ui/motion';
<Hero layout="overlay" height="half">
{#snippet media()}<img src="…" alt="" />{/snippet}
{#snippet title()}
<span {@attach reveal({ delay: 0 })}>Throw straighter, sooner</span>
{/snippet}
{#snippet subtitle()}
<span {@attach reveal({ delay: 160 })}>One drill at a time.</span>
{/snippet}
{#snippet actions()}
<span {@attach reveal({ delay: 320 })}>
<Button>Start the course</Button>
</span>
{/snippet}
</Hero>
<style>
/* transform needs a block box: reveal is a no-op on inline spans */
span { display: block; }
</style>Why reveal and not revealGroup here
revealGroup staggers a container's direct children, and the parts you
want sequenced are children of Hero's internal .hz-hero-content, which
the component does not expose, since {...rest} spreads onto its root.
Hero's snippet props are the way in: the markup inside them is yours, so reveal attaches to it and delay supplies the sequence. Attaching
to the Hero root instead animates the whole thing as one block.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.
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
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.
@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