Skip to content
LyEve UI v0.60.1

Foundations

Motion

Every component moves on the same four durations and three curves, declared in styles.css beside the palette. Each is named for what it is used for, so the choice is made by reading the name.

Durations

TokenValueFor
--duration-fast120msa state change in place: color, opacity, border, shadow, focus
--duration-base200msmovement in place (a knob, a chevron, a collapse) and a small surface arriving (menu, popover, tooltip, toast)
--duration-slow320msa large surface arriving (dialog, drawer)
--duration-progress500msa value moving (a progress bar's width)

Curves

TokenValueFor
--ease-entercubic-bezier(0.22, 1, 0.36, 1)arriving: starts fast and settles
--ease-exitcubic-bezier(0.32, 0, 0.67, 0)leaving: starts slow and accelerates away
--ease-movecubic-bezier(0.65, 0, 0.35, 1)changing in place

One rule for exits

An exit runs one rung faster than its entrance, on the exit curve: slow in, base out. Base in, fast out. Entrances and exits move only opacity and transform. Nothing here needs deciding per component. The presets below derive the exit from the entrance.

In a class attribute

The transition-* utilities default to the fast rung and the move curve, so transition-colors on its own is complete, and so is transition-shadow for an elevation change. For the other rungs and curves the kit ships duration-base, duration-slow, duration-progress, ease-enter, ease-exit and ease-move.

A disclosure animates the one property that opens it. Downwards that is grid-template-rows, the collapsible pattern. Sideways it is width, and it takes the same rung and curve, which is what the app shell's sidebar does between its rail and its expanded column. Nothing else animates layout.

<button class="transition-colors hover:bg-surface-2">Save</button>

<svg class="transition-transform duration-base {open ? 'rotate-180' : ''}">...</svg>

<aside class="transition-[width] duration-base ease-move {rail ? 'w-nav-rail' : 'w-sidebar'}"></aside>

<div class="h-full transition-[width] duration-progress" style="width: {pct}%"></div>

Do not write a numbered duration such as duration-150, an arbitrary one such as duration-[250ms], the curves ease-in, ease-out, ease-in-out and ease-linear, or transition-all. A duration of your own is a fifth speed nobody chose, those curves are ones the theme never states, and transition-all animates whatever happens to change, layout included.

A surface that mounts and unmounts

A surface inside {#if} enters and leaves through the motion presets, the same ones the kit's own overlays use. They read the tokens at run time, so retuning a token retunes them, and they play nothing for a reader who has asked for reduced motion. Keep the |global modifier: a Svelte transition is local by default and would not play when a parent block removes the surface.

Never wait for an animation with a timer. A setTimeout that holds an element until its exit has played is the duration written a second time, and it drifts the day the token changes. Remove the element and let its outro play.

<script>
  import { motion } from '@lyeve-labs/ui-kit';
</script>

{#if open}
  <div transition:motion.popover|global>...</div>
{/if}

{#each items as item (item.id)}
  <li transition:motion.toast|global animate:motion.reorder>...</li>
{/each}
A panel on the popover preset: base in, fast out.
A popover
PresetInOutFrame
dialogslow, enterbase, exitfades, rises 8px, scales from 0.96
scrimslow, enterbase, exitfades
drawerslow, enterbase, exitslides the whole panel in from its edge. Takes a side
popoverbase, enterfast, exitfades, drops 4px, scales from 0.96
toastbase, enterfast, exitfades, rises 8px, scales from 0.96
reorderbase, moveFor animate:. Siblings close the gap a removed item leaves.

A change of page

A page is a parent block too, so every surface on it plays its exit when the reader leaves the page, and Svelte keeps the page on screen until the last exit ends, with the new page below it. Call motion.swapPage() just before the router replaces the page. Until the next frame every preset runs in no time, so the old page goes and the new one arrives in the same frame.

<script>
  import { onNavigate } from '$app/navigation';
  import { motion } from '@lyeve-labs/ui-kit';

  onNavigate((navigation) => {
    if (navigation.from?.url.pathname !== navigation.to?.url.pathname) motion.swapPage();
  });
</script>

Reduced motion

A reader who has asked for reduced motion gets every state change and none of the travel. The stylesheet shortens every CSS transition and animation to 1ms, so a component that waits on an animationend still hears it, and the presets return a zero duration, so a surface mounts and unmounts at once.

That one rule in the kit's stylesheet is the only prefers-reduced-motion block. A component or a page never writes a second one: the global rule already covers it, and a second block is a place where the two can disagree.