Skip to content

Motion

MotionStage

The frame of a motion demo: plays only in view, hands control to the user on touch, always pauses, and renders the final state under reduced motion.

<script>
  import { MotionStage } from "@codeatlasdev/ui";
</script>

<MotionStage label="Fade rise: a card fades in while rising" loop={2400}>
  {#snippet children(m)}
    {#key m.cycle}
      <div class:rise={!m.reduced}>Invoice paid</div>
    {/key}
  {/snippet}
</MotionStage>

MotionStage owns the timing contract so a demo only has to draw. It starts when at least half of it is in view and stops when it leaves the viewport or the tab is hidden, so a page full of motion never animates everything at once. Pointer, key or focus inside the demo takes over: autoplay stops, the demo stays interactive and a Replay button appears. A Pause button (aria-pressed while stopped) is always there while motion can run (WCAG 2.2.2). Under prefers-reduced-motion: reduce, or in a theme with motion: { enter: "none" } (--ca-enter-scale: 0), nothing autoplays, the buttons are not rendered and m.reduced is true: render the final state.

The snippet receives m with playing, reduced, cycle, takeover(), play() and pause(). cycle increments on every loop and on Replay: key the animated element on it ({#key m.cycle}) so the animation runs again. Keep the animation in CSS and never start your own timers.

Fade rise

The reference demo: it loops while in view. Touch it to take over, or press Pause.

Code
svelte
<script lang="ts">
  import { Card, MotionStage } from '@codeatlasdev/ui';
</script>

<MotionStage label="Fade rise: a card fades in while rising into place" loop={2400}>
  {#snippet children(m)}
    <!-- Keyed on the cycle: a new cycle mounts a fresh element, so the animation runs again. -->
    {#key m.cycle}
      <div class="rise" class:rise--run={!m.reduced}>
        <Card>
          <p class="rise__title">Invoice paid</p>
          <p class="rise__body">The card fades in and rises 16px into place.</p>
        </Card>
      </div>
    {/key}
  {/snippet}
</MotionStage>

<style>
  .rise { width: 100%; max-width: 320px; }
  .rise--run { animation: fade-rise 420ms cubic-bezier(0.2, 0, 0, 1) 80ms both; }
  .rise__title { margin: 0; font-weight: var(--ca-font-weight-semibold); }
  .rise__body { margin: var(--ca-space-1) 0 0; color: var(--ca-text-secondary); }
  @keyframes fade-rise {
    from { opacity: 0; transform: translateY(16px); }
    to { opacity: 1; transform: translateY(0); }
  }
</style>

Props

PropTypeDefaultDescription
labelstring–Accessible name of the stage (the group label). Say what the demo shows.
loopnumber3200Autoplay loop length in milliseconds. `cycle` increments once per loop.
pauseLabelstring–Label of the pause button. Defaults to the i18n catalog (`motionStage.pause`).
replayLabelstring–Label of the replay button. Defaults to the i18n catalog (`motionStage.replay`).
childrenSnippet<[MotionCtx]>–The demo. Receives `{ playing, reduced, cycle, takeover, play, pause }`.