System
PageTransition
Page transitions — off unless you ask for one.
import { PageTransition } from 'omaris' Examples
Presets
Five of the seventeen presets, off unless you ask. Change key and the content transitions; here the key is the step, so the buttons drive it.
<script lang="ts"> import { Button, PageTransition, SegmentedButton } from 'omaris'; import type { PageTransitionName } from 'omaris'; let transition = $state<PageTransitionName>('shared-x'); let step = $state(0); const PANELS = ['Overview', 'Orders', 'Customers'];</script><div class="flex w-full max-w-lg flex-col gap-4"> <SegmentedButton label="Preset" bind:value={transition} items={[ { value: 'fade', label: 'fade' }, { value: 'fade-through', label: 'fade-through' }, { value: 'shared-x', label: 'shared-x' }, { value: 'shared-y', label: 'shared-y' }, { value: 'scale', label: 'scale' } ]} /> <div class="h-32 overflow-hidden rounded-shape-lg border border-border"> <PageTransition key={step} {transition} class="h-full"> <div class="flex h-full items-center justify-center bg-surface-container-low"> <span class="text-title-md">{PANELS[step]}</span> </div> </PageTransition> </div> <div class="flex gap-2"> <Button size="xs" variant="outlined" onclick={() => (step = (step + 2) % 3)}>Back</Button> <Button size="xs" variant="outlined" onclick={() => (step = (step + 1) % 3)}>Forward</Button> </div></div> In a layout
In a real app the key is the pathname, so every navigation transitions and nothing else has to change: ``svelte <PageTransition key={page.url.pathname} transition="shared-x"> {@render children()} </PageTransition> ` direction` is what makes a back button feel like going back.
/orders<script lang="ts"> import { Button, PageTransition } from 'omaris'; let index = $state(0); let direction = $state<'forward' | 'backward'>('forward'); const PAGES = ['/orders', '/orders/4021', '/orders/4021/items']; function go(step: number) { direction = step > 0 ? 'forward' : 'backward'; index = Math.min(PAGES.length - 1, Math.max(0, index + step)); }</script><div class="flex w-full max-w-lg flex-col gap-4"> <div class="h-28 overflow-hidden rounded-shape-lg border border-border"> <PageTransition key={PAGES[index]} transition="shared-x" {direction} class="h-full"> <div class="flex h-full items-center justify-center bg-surface-container-low"> <code class="text-body-md">{PAGES[index]}</code> </div> </PageTransition> </div> <div class="flex gap-2"> <Button size="xs" variant="outlined" disabled={index === 0} onclick={() => go(-1)}>Back</Button> <Button size="xs" variant="outlined" disabled={index === 2} onclick={() => go(1)}> Deeper </Button> </div></div> Native stack
push is the phone's navigation stack: the arriving page slides in over the old one, which parallaxes a third of the way and dims behind it. cover rises over a page that stays put, and reveal slides the top one away.
/orders<script lang="ts"> import { Button, PageTransition, SegmentedButton } from 'omaris'; import type { PageTransitionName } from 'omaris'; let transition = $state<PageTransitionName>('push'); let index = $state(0); const PAGES = ['/orders', '/orders/4021', '/orders/4021/items']; function go(step: number) { index = Math.min(PAGES.length - 1, Math.max(0, index + step)); }</script><div class="flex w-full max-w-lg flex-col gap-4"> <SegmentedButton label="Pattern" bind:value={transition} items={[ { value: 'push', label: 'push' }, { value: 'cover', label: 'cover' }, { value: 'reveal', label: 'reveal' }, { value: 'swap', label: 'swap' } ]} /> <div class="h-28 overflow-hidden rounded-shape-lg border border-border"> <PageTransition key={PAGES[index]} {transition} direction="auto" class="h-full"> <div class="flex h-full items-center justify-center bg-surface-container-low"> <code class="text-body-md">{PAGES[index]}</code> </div> </PageTransition> </div> <div class="flex gap-2"> <Button size="xs" variant="outlined" disabled={index === 0} onclick={() => go(-1)}>Back</Button> <Button size="xs" variant="outlined" disabled={index === 2} onclick={() => go(1)}> Deeper </Button> </div></div> Auto direction
direction="auto" keeps the keys it has seen as a stack: a key it has never seen plays forward, and one already on the stack plays backward and unwinds it. Click across the tabs, then back to one you have been to.
<script lang="ts"> import { PageTransition, SegmentedButton, Text } from 'omaris'; const TABS = ['Overview', 'Orders', 'Menu', 'Staff']; let at = $state('Overview'); let played = $state('—');</script><div class="flex w-full max-w-lg flex-col gap-4"> <SegmentedButton label="Section" bind:value={at} items={TABS.map((tab) => ({ value: tab, label: tab }))} /> <div class="h-28 overflow-hidden rounded-shape-lg border border-border"> <PageTransition key={at} transition="shared-x" direction="auto" class="h-full" onstart={(way) => (played = way)} > <div class="flex h-full items-center justify-center bg-surface-container-low"> <span class="text-title-md">{at}</span> </div> </PageTransition> </div> <Text variant="label-sm" tone="muted">Last transition played: {played}</Text></div> When to use it
Use it for
- Route changes that should feel like an app. In
+layout.svelte, setkey={page.url.pathname}andtransition="shared-x". A wizard or a settings screen keys on the step instead of the URL. - A stack like a phone's.
transition="push"withdirection="auto"slides a deeper page in over the old one and plays the reverse on the way back. - Going back that reads as going back.
direction="auto"keeps the keys it has seen as a stack, so a Back button, a breadcrumb and a swipe back all play backward with no wiring. - Pages of different heights. The leaving page is taken out of flow and the container clips, so the document never grows a scrollbar.
- A keyboard that should follow.
focusmoves focus to the arriving page;scroll="top"resets the scroll like a native screen.
Do
- Pick one preset and keep it.
shared-xfor siblings,pushfor a stack,shared-y,zoomorscalefor parent to child,fade-throughfor unrelated destinations,fadeordissolvefor the least motion. - Leave
durationalone unless you disagree with the preset.pushis longer thanfadebecause it travels further. - Prefer
direction="auto". Set it by hand only where the keys are not a stack, like a tab strip. - Key on the pathname, not the whole URL, so a query-string change does not replay the page.
- Trust the presets under RTL.
shared-x,slide,pushandswapread the writing direction, so forward is leftward in Arabic.
Don't
- Put one in every layout. Use one, at the outermost layout that changes.
- Wrap a
TopAppBaror aNavigationDrawerin it. Only the page moves. - Key on an object. A new object every render is a transition every render.
- Use it for a change inside a page, like a filter or a sort.
- Use
fliptwice. It draws attention to itself.
API
PageTransition
Page transitions — off unless you ask for one.
transition defaults to false, and a false transition renders the children with no wrapper, no keyed block and no cost: navigation behaves exactly as it did before this component existed. Name a pattern and the page starts moving, on the same MD3 curves the components use.
— where page is SvelteKit's $app/state, so every navigation is a new key and every key change is a transition.
`direction="auto"` works out which way you went. It keeps the keys it has seen as a stack: a key it has never seen is forward and goes on the stack, and a key already on it is a Back — the stack unwinds to it and the pattern plays in reverse. That is a browser Back button, a swipe back and a breadcrumb, all without the layout being told which.
Seventeen patterns, each with the duration it actually wants, so duration is only there for when you disagree. Patterns that travel along the inline axis follow the writing direction, so forward is leftward in Arabic without a second preset.
The two pages are stacked in one grid cell while they cross, so nothing jumps and the scroll position doesn't lurch. The leaving page is made inert on its way out, so a click can't land on a page that's gone.
Neither page may move the document while they cross. Two things would, and both are handled here rather than left to the consumer:
- The pair's height. Stacked in one grid cell, the row is as tall as the taller of the two, so a tall page leaving a short one holds the document at the old height and then drops it — a scrollbar that appears for a few frames and goes. The leaving page is taken out of flow at its own size the moment it starts to leave, so the container is only ever the height of the page arriving. - The travel. A page translated 32px or a full width is still part of the scrollable overflow, so the axis it moves along grows a scrollbar mid-flight. The container clips while a transition is running and stops clipping the moment it ends, so a menu or a sticky header that overflows on purpose is unaffected the rest of the time.
import { PageTransition } from 'omaris' <PageTransition key={page.url.pathname} transition="push" direction="auto"> {@render children()}</PageTransition> Props
key unknown Change this to run a transition — page.url.pathname, usually.
transition Defaults to false
PageTransitionName | false The pattern, or false (the default) for none at all.
direction Defaults to 'forward'
PageTransitionDirection | 'auto' Which way the axis patterns travel. auto reads it off the keys it has seen: a new one is forward, one it has been to before is a Back.
duration number Total time for the pair, in ms. Defaults to what the pattern wants.
clip Defaults to true
boolean Clip the travel while a transition runs. On unless a page has something that has to escape the container mid-flight.
scroll Defaults to 'keep'
'keep' | 'top' Put the scroll back to the top as the new page arrives. 'top' scrolls the nearest scrollable ancestor, which is the app frame in a dashboard and the window on an ordinary page.
focus Defaults to false
boolean Move focus to the arriving page, the way a native screen change does, so a keyboard lands on the new page rather than back at the top of the document.
onstart (direction: PageTransitionDirection) => void Called as a pair starts, with the direction it is playing.
onend () => void Called once the pair has landed.
children required Snippet The page.
class string