Kosmesis
Components

Motion

An animate-on-mount / animate-on-scroll-into-view wrapper with 28 built-in effect presets. Purely presentational — no Morphos equivalent.

Installation

npx kosmesis add motion
pnpm dlx kosmesis add motion
yarn dlx kosmesis add motion
bunx kosmesis add motion

Copy and paste the following code into your project.

motion.tsx
import { StatefulComponent } from "@praxisjs/core";import { Component, OnCommand, Prop, Ref, State, Watch, type Command, type Ref as RefType } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { cn } from "@/lib/utils";const DIST_SM = 20;const DIST_LG = 56;const ROT_SM = 10;const ROT_LG = 25;const FLIP_DEG = 80;const FLIP_TILT_DEG = 55;const SKEW_DEG = 10;const BLUR_PX = 10;const EASE_BOUNCE = "cubic-bezier(0.34, 1.56, 0.64, 1)";const EASE_ELASTIC = "cubic-bezier(0.68, -0.55, 0.27, 1.55)";export interface MotionStyle {  opacity?: number;  x?: number;  y?: number;  scale?: number;  scaleX?: number;  scaleY?: number;  rotate?: number;  rotateX?: number;  rotateY?: number;  skewX?: number;  skewY?: number;  /** Gaussian blur radius in px. */  blur?: number;}function toTransform(style: MotionStyle, perspective: number): string {  const parts: string[] = [];  if (style.rotateX !== undefined || style.rotateY !== undefined) parts.push(`perspective(${String(perspective)}px)`);  if (style.x !== undefined || style.y !== undefined) parts.push(`translate(${String(style.x ?? 0)}px, ${String(style.y ?? 0)}px)`);  if (style.scale !== undefined) parts.push(`scale(${String(style.scale)})`);  if (style.scaleX !== undefined) parts.push(`scaleX(${String(style.scaleX)})`);  if (style.scaleY !== undefined) parts.push(`scaleY(${String(style.scaleY)})`);  if (style.rotate !== undefined) parts.push(`rotate(${String(style.rotate)}deg)`);  if (style.rotateX !== undefined) parts.push(`rotateX(${String(style.rotateX)}deg)`);  if (style.rotateY !== undefined) parts.push(`rotateY(${String(style.rotateY)}deg)`);  if (style.skewX !== undefined) parts.push(`skewX(${String(style.skewX)}deg)`);  if (style.skewY !== undefined) parts.push(`skewY(${String(style.skewY)}deg)`);  return parts.length > 0 ? parts.join(" ") : "none";}// Direction suffixes describe where the element starts relative to its settled position —// `"fade-up"` starts below and rises into place, `"fade-left"` starts right and slides left.export type MotionEffect =  | "fade"  | "fade-up"  | "fade-down"  | "fade-left"  | "fade-right"  | "slide-up"  | "slide-down"  | "slide-left"  | "slide-right"  | "zoom-in"  | "zoom-out"  | "flip-x"  | "flip-y"  | "flip-up"  | "flip-down"  | "rotate-in"  | "rotate-left"  | "rotate-right"  | "skew-up"  | "skew-down"  | "blur-in"  | "bounce-in"  | "bounce-up"  | "elastic-in"  | "pop"  | "roll-in-left"  | "roll-in-right"  | "drop-in";interface MotionEffectDef {  from: MotionStyle;  to: MotionStyle;  easing?: string;}const MOTION_EFFECTS: Record<MotionEffect, MotionEffectDef> = {  fade: { from: { opacity: 0 }, to: { opacity: 1 } },  "fade-up": { from: { opacity: 0, y: DIST_SM }, to: { opacity: 1, y: 0 } },  "fade-down": { from: { opacity: 0, y: -DIST_SM }, to: { opacity: 1, y: 0 } },  "fade-left": { from: { opacity: 0, x: DIST_SM }, to: { opacity: 1, x: 0 } },  "fade-right": { from: { opacity: 0, x: -DIST_SM }, to: { opacity: 1, x: 0 } },  "slide-up": { from: { y: DIST_LG }, to: { y: 0 } },  "slide-down": { from: { y: -DIST_LG }, to: { y: 0 } },  "slide-left": { from: { x: DIST_LG }, to: { x: 0 } },  "slide-right": { from: { x: -DIST_LG }, to: { x: 0 } },  "zoom-in": { from: { opacity: 0, scale: 0.85 }, to: { opacity: 1, scale: 1 } },  "zoom-out": { from: { opacity: 0, scale: 1.15 }, to: { opacity: 1, scale: 1 } },  "flip-x": { from: { opacity: 0, rotateX: FLIP_DEG }, to: { opacity: 1, rotateX: 0 } },  "flip-y": { from: { opacity: 0, rotateY: FLIP_DEG }, to: { opacity: 1, rotateY: 0 } },  "flip-up": { from: { opacity: 0, rotateX: -FLIP_TILT_DEG, y: 16 }, to: { opacity: 1, rotateX: 0, y: 0 } },  "flip-down": { from: { opacity: 0, rotateX: FLIP_TILT_DEG, y: -16 }, to: { opacity: 1, rotateX: 0, y: 0 } },  "rotate-in": { from: { opacity: 0, rotate: -ROT_SM }, to: { opacity: 1, rotate: 0 } },  "rotate-left": { from: { opacity: 0, rotate: -ROT_LG, x: 16 }, to: { opacity: 1, rotate: 0, x: 0 } },  "rotate-right": { from: { opacity: 0, rotate: ROT_LG, x: -16 }, to: { opacity: 1, rotate: 0, x: 0 } },  "skew-up": { from: { opacity: 0, skewY: -SKEW_DEG, y: 16 }, to: { opacity: 1, skewY: 0, y: 0 } },  "skew-down": { from: { opacity: 0, skewY: SKEW_DEG, y: -16 }, to: { opacity: 1, skewY: 0, y: 0 } },  "blur-in": { from: { opacity: 0, blur: BLUR_PX }, to: { opacity: 1, blur: 0 } },  "bounce-in": { from: { opacity: 0, scale: 0.3 }, to: { opacity: 1, scale: 1 }, easing: EASE_BOUNCE },  "bounce-up": { from: { opacity: 0, y: DIST_LG, scale: 0.9 }, to: { opacity: 1, y: 0, scale: 1 }, easing: EASE_BOUNCE },  "elastic-in": { from: { opacity: 0, scale: 0.5 }, to: { opacity: 1, scale: 1 }, easing: EASE_ELASTIC },  pop: { from: { opacity: 0, scale: 0.92 }, to: { opacity: 1, scale: 1 }, easing: EASE_BOUNCE },  "roll-in-left": { from: { opacity: 0, x: -DIST_LG, rotate: -90 }, to: { opacity: 1, x: 0, rotate: 0 } },  "roll-in-right": { from: { opacity: 0, x: DIST_LG, rotate: 90 }, to: { opacity: 1, x: 0, rotate: 0 } },  "drop-in": { from: { opacity: 0, y: -DIST_LG * 1.2, scale: 0.9 }, to: { opacity: 1, y: 0, scale: 1 }, easing: EASE_BOUNCE },};const DEFAULT_FROM: MotionStyle = { opacity: 0, y: 12 };const DEFAULT_TO: MotionStyle = { opacity: 1, y: 0 };export interface MotionProps {  /** A named preset (28 built in) that fills in `from`/`to`/easing — explicit `from`/`to`/`easing` still win. */  effect?: MotionEffect;  from?: MotionStyle;  to?: MotionStyle;  /** Overrides the effect's suggested easing (or `"ease"` for presets that don't suggest one). */  easing?: string;  /** `perspective(px)` applied whenever `rotateX`/`rotateY` is used, for a real 3D-feeling flip on a single element. */  perspective?: number;  duration?: number;  delay?: number;  once?: boolean;  inView?: boolean;  /** Ping-pongs between `from`/`to` forever instead of settling once — ignores `once`/`inView`. Reactive: toggling it after mount starts/stops the loop. */  loop?: boolean;  /** A `@Command()` field from the parent — calling `trigger()` replays the animation on demand. */  trigger?: Command;  class?: string;  id?: string;  children?: Children;}@Component()export class Motion extends StatefulComponent {  @Prop() effect?: MotionEffect;  @Prop() from?: MotionStyle;  @Prop() to?: MotionStyle;  @Prop() easing?: string;  @Prop() perspective = 800;  @Prop() duration = 500;  @Prop() delay = 0;  @Prop() once = true;  @Prop() inView = true;  @Prop() loop = false;  @Prop() trigger?: Command;  @Prop() class?: string;  @Prop() id?: string;  @Prop() children?: Children;  @Ref<HTMLDivElement>()  elRef!: RefType<HTMLDivElement>;  @State() _active = false;  @State() _replaying = false;  private _observer?: IntersectionObserver;  private _loopTimer?: ReturnType<typeof setInterval>;  onMount(): void {    this._syncLoop();    if (this.loop) return;    if (!this.inView) {      requestAnimationFrame(() => {        this._active = true;      });      return;    }    const el = this.elRef.current;    if (!el) return;    this._observer = new IntersectionObserver((entries) => {      for (const entry of entries) {        if (entry.isIntersecting) {          this._active = true;          if (this.once) this._observer?.disconnect();        } else if (!this.once) {          this._active = false;        }      }    });    this._observer.observe(el);  }  onUnmount(): void {    this._observer?.disconnect();    clearInterval(this._loopTimer);  }  /** Runs once from `onMount` for the initial value, then again on every later `loop` change. */  @Watch("loop")  private _syncLoop(): void {    clearInterval(this._loopTimer);    if (!this.loop) {      this._active = true;      return;    }    requestAnimationFrame(() => {      this._active = true;    });    this._loopTimer = setInterval(() => {      this._active = !this._active;    }, this.duration + this.delay);  }  @OnCommand("trigger")  private _replay(): void {    // A transition is declared on the same style object as the target values, so merely    // flipping `_active` false-then-true isn't enough: without disabling the transition first,    // the "false" write starts a `duration`-long transition toward `from`, which the "true"    // write then interrupts moments later — a barely visible blip, not a replay. Disable the    // transition, snap to `from`, let that paint, then re-enable it and animate to `to`.    this._replaying = true;    this._active = false;    requestAnimationFrame(() => {      requestAnimationFrame(() => {        this._replaying = false;        this._active = true;      });    });  }  private get _resolved(): { from: MotionStyle; to: MotionStyle; easing: string } {    const preset = this.effect ? MOTION_EFFECTS[this.effect] : undefined;    return {      from: this.from ?? preset?.from ?? DEFAULT_FROM,      to: this.to ?? preset?.to ?? DEFAULT_TO,      easing: this.easing ?? preset?.easing ?? "ease",    };  }  render() {    return (      <div        ref={this.elRef}        id={this.id}        data-slot="motion"        class={cn("will-change-transform", this.class)}        style={() => {          const { from, to, easing } = this._resolved;          const state = this._active ? to : from;          const transition = this._replaying            ? "none"            : `opacity ${String(this.duration)}ms ${easing} ${String(this.delay)}ms, transform ${String(this.duration)}ms ${easing} ${String(this.delay)}ms, filter ${String(this.duration)}ms ${easing} ${String(this.delay)}ms`;          return {            opacity: state.opacity ?? 1,            transform: toTransform(state, this.perspective),            filter: state.blur !== undefined ? `blur(${String(state.blur)}px)` : "none",            transition,          };        }}      >        {this.children}      </div>    );  }}

Install the following dependencies:

npm install @praxisjs/css
pnpm add @praxisjs/css
yarn add @praxisjs/css
bun add @praxisjs/css

Copy and paste the following code into your project.

motion.tsx
import { StatefulComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled } from "@praxisjs/css";import { Component, OnCommand, Prop, Ref, State, Watch, type Command, type Ref as RefType } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";const DIST_SM = 20;const DIST_LG = 56;const ROT_SM = 10;const ROT_LG = 25;const FLIP_DEG = 80;const FLIP_TILT_DEG = 55;const SKEW_DEG = 10;const BLUR_PX = 10;const EASE_BOUNCE = "cubic-bezier(0.34, 1.56, 0.64, 1)";const EASE_ELASTIC = "cubic-bezier(0.68, -0.55, 0.27, 1.55)";class MotionStyles extends Stylesheet {  $root = this.css({ willChange: "transform" });}export interface MotionStyle {  opacity?: number;  x?: number;  y?: number;  scale?: number;  scaleX?: number;  scaleY?: number;  rotate?: number;  rotateX?: number;  rotateY?: number;  skewX?: number;  skewY?: number;  /** Gaussian blur radius in px. */  blur?: number;}function toTransform(style: MotionStyle, perspective: number): string {  const parts: string[] = [];  if (style.rotateX !== undefined || style.rotateY !== undefined) parts.push(`perspective(${String(perspective)}px)`);  if (style.x !== undefined || style.y !== undefined) parts.push(`translate(${String(style.x ?? 0)}px, ${String(style.y ?? 0)}px)`);  if (style.scale !== undefined) parts.push(`scale(${String(style.scale)})`);  if (style.scaleX !== undefined) parts.push(`scaleX(${String(style.scaleX)})`);  if (style.scaleY !== undefined) parts.push(`scaleY(${String(style.scaleY)})`);  if (style.rotate !== undefined) parts.push(`rotate(${String(style.rotate)}deg)`);  if (style.rotateX !== undefined) parts.push(`rotateX(${String(style.rotateX)}deg)`);  if (style.rotateY !== undefined) parts.push(`rotateY(${String(style.rotateY)}deg)`);  if (style.skewX !== undefined) parts.push(`skewX(${String(style.skewX)}deg)`);  if (style.skewY !== undefined) parts.push(`skewY(${String(style.skewY)}deg)`);  return parts.length > 0 ? parts.join(" ") : "none";}// Direction suffixes describe where the element starts relative to its settled position —// `"fade-up"` starts below and rises into place, `"fade-left"` starts right and slides left.export type MotionEffect =  | "fade"  | "fade-up"  | "fade-down"  | "fade-left"  | "fade-right"  | "slide-up"  | "slide-down"  | "slide-left"  | "slide-right"  | "zoom-in"  | "zoom-out"  | "flip-x"  | "flip-y"  | "flip-up"  | "flip-down"  | "rotate-in"  | "rotate-left"  | "rotate-right"  | "skew-up"  | "skew-down"  | "blur-in"  | "bounce-in"  | "bounce-up"  | "elastic-in"  | "pop"  | "roll-in-left"  | "roll-in-right"  | "drop-in";interface MotionEffectDef {  from: MotionStyle;  to: MotionStyle;  easing?: string;}const MOTION_EFFECTS: Record<MotionEffect, MotionEffectDef> = {  fade: { from: { opacity: 0 }, to: { opacity: 1 } },  "fade-up": { from: { opacity: 0, y: DIST_SM }, to: { opacity: 1, y: 0 } },  "fade-down": { from: { opacity: 0, y: -DIST_SM }, to: { opacity: 1, y: 0 } },  "fade-left": { from: { opacity: 0, x: DIST_SM }, to: { opacity: 1, x: 0 } },  "fade-right": { from: { opacity: 0, x: -DIST_SM }, to: { opacity: 1, x: 0 } },  "slide-up": { from: { y: DIST_LG }, to: { y: 0 } },  "slide-down": { from: { y: -DIST_LG }, to: { y: 0 } },  "slide-left": { from: { x: DIST_LG }, to: { x: 0 } },  "slide-right": { from: { x: -DIST_LG }, to: { x: 0 } },  "zoom-in": { from: { opacity: 0, scale: 0.85 }, to: { opacity: 1, scale: 1 } },  "zoom-out": { from: { opacity: 0, scale: 1.15 }, to: { opacity: 1, scale: 1 } },  "flip-x": { from: { opacity: 0, rotateX: FLIP_DEG }, to: { opacity: 1, rotateX: 0 } },  "flip-y": { from: { opacity: 0, rotateY: FLIP_DEG }, to: { opacity: 1, rotateY: 0 } },  "flip-up": { from: { opacity: 0, rotateX: -FLIP_TILT_DEG, y: 16 }, to: { opacity: 1, rotateX: 0, y: 0 } },  "flip-down": { from: { opacity: 0, rotateX: FLIP_TILT_DEG, y: -16 }, to: { opacity: 1, rotateX: 0, y: 0 } },  "rotate-in": { from: { opacity: 0, rotate: -ROT_SM }, to: { opacity: 1, rotate: 0 } },  "rotate-left": { from: { opacity: 0, rotate: -ROT_LG, x: 16 }, to: { opacity: 1, rotate: 0, x: 0 } },  "rotate-right": { from: { opacity: 0, rotate: ROT_LG, x: -16 }, to: { opacity: 1, rotate: 0, x: 0 } },  "skew-up": { from: { opacity: 0, skewY: -SKEW_DEG, y: 16 }, to: { opacity: 1, skewY: 0, y: 0 } },  "skew-down": { from: { opacity: 0, skewY: SKEW_DEG, y: -16 }, to: { opacity: 1, skewY: 0, y: 0 } },  "blur-in": { from: { opacity: 0, blur: BLUR_PX }, to: { opacity: 1, blur: 0 } },  "bounce-in": { from: { opacity: 0, scale: 0.3 }, to: { opacity: 1, scale: 1 }, easing: EASE_BOUNCE },  "bounce-up": { from: { opacity: 0, y: DIST_LG, scale: 0.9 }, to: { opacity: 1, y: 0, scale: 1 }, easing: EASE_BOUNCE },  "elastic-in": { from: { opacity: 0, scale: 0.5 }, to: { opacity: 1, scale: 1 }, easing: EASE_ELASTIC },  pop: { from: { opacity: 0, scale: 0.92 }, to: { opacity: 1, scale: 1 }, easing: EASE_BOUNCE },  "roll-in-left": { from: { opacity: 0, x: -DIST_LG, rotate: -90 }, to: { opacity: 1, x: 0, rotate: 0 } },  "roll-in-right": { from: { opacity: 0, x: DIST_LG, rotate: 90 }, to: { opacity: 1, x: 0, rotate: 0 } },  "drop-in": { from: { opacity: 0, y: -DIST_LG * 1.2, scale: 0.9 }, to: { opacity: 1, y: 0, scale: 1 }, easing: EASE_BOUNCE },};const DEFAULT_FROM: MotionStyle = { opacity: 0, y: 12 };const DEFAULT_TO: MotionStyle = { opacity: 1, y: 0 };export interface MotionProps {  /** A named preset (28 built in) that fills in `from`/`to`/easing — explicit `from`/`to`/`easing` still win. */  effect?: MotionEffect;  from?: MotionStyle;  to?: MotionStyle;  /** Overrides the effect's suggested easing (or `"ease"` for presets that don't suggest one). */  easing?: string;  /** `perspective(px)` applied whenever `rotateX`/`rotateY` is used, for a real 3D-feeling flip on a single element. */  perspective?: number;  duration?: number;  delay?: number;  once?: boolean;  inView?: boolean;  /** Ping-pongs between `from`/`to` forever instead of settling once — ignores `once`/`inView`. Reactive: toggling it after mount starts/stops the loop. */  loop?: boolean;  /** A `@Command()` field from the parent — calling `trigger()` replays the animation on demand. */  trigger?: Command;  class?: string;  id?: string;  children?: Children;}@Component()export class Motion extends StatefulComponent {  @Styled(MotionStyles) $s!: MotionStyles;  @Prop() effect?: MotionEffect;  @Prop() from?: MotionStyle;  @Prop() to?: MotionStyle;  @Prop() easing?: string;  @Prop() perspective = 800;  @Prop() duration = 500;  @Prop() delay = 0;  @Prop() once = true;  @Prop() inView = true;  @Prop() loop = false;  @Prop() trigger?: Command;  @Prop() class?: string;  @Prop() id?: string;  @Prop() children?: Children;  @Ref<HTMLDivElement>()  elRef!: RefType<HTMLDivElement>;  @State() _active = false;  @State() _replaying = false;  private _observer?: IntersectionObserver;  private _loopTimer?: ReturnType<typeof setInterval>;  onMount(): void {    this._syncLoop();    if (this.loop) return;    if (!this.inView) {      requestAnimationFrame(() => {        this._active = true;      });      return;    }    const el = this.elRef.current;    if (!el) return;    this._observer = new IntersectionObserver((entries) => {      for (const entry of entries) {        if (entry.isIntersecting) {          this._active = true;          if (this.once) this._observer?.disconnect();        } else if (!this.once) {          this._active = false;        }      }    });    this._observer.observe(el);  }  onUnmount(): void {    this._observer?.disconnect();    clearInterval(this._loopTimer);  }  /** Runs once from `onMount` for the initial value, then again on every later `loop` change. */  @Watch("loop")  private _syncLoop(): void {    clearInterval(this._loopTimer);    if (!this.loop) {      this._active = true;      return;    }    requestAnimationFrame(() => {      this._active = true;    });    this._loopTimer = setInterval(() => {      this._active = !this._active;    }, this.duration + this.delay);  }  @OnCommand("trigger")  private _replay(): void {    // A transition is declared on the same style object as the target values, so merely    // flipping `_active` false-then-true isn't enough: without disabling the transition first,    // the "false" write starts a `duration`-long transition toward `from`, which the "true"    // write then interrupts moments later — a barely visible blip, not a replay. Disable the    // transition, snap to `from`, let that paint, then re-enable it and animate to `to`.    this._replaying = true;    this._active = false;    requestAnimationFrame(() => {      requestAnimationFrame(() => {        this._replaying = false;        this._active = true;      });    });  }  private get _resolved(): { from: MotionStyle; to: MotionStyle; easing: string } {    const preset = this.effect ? MOTION_EFFECTS[this.effect] : undefined;    return {      from: this.from ?? preset?.from ?? DEFAULT_FROM,      to: this.to ?? preset?.to ?? DEFAULT_TO,      easing: this.easing ?? preset?.easing ?? "ease",    };  }  render() {    return (      <div        ref={this.elRef}        id={this.id}        data-slot="motion"        class={cx(this.$s.$root, this.class)}        style={() => {          const { from, to, easing } = this._resolved;          const state = this._active ? to : from;          const transition = this._replaying            ? "none"            : `opacity ${String(this.duration)}ms ${easing} ${String(this.delay)}ms, transform ${String(this.duration)}ms ${easing} ${String(this.delay)}ms, filter ${String(this.duration)}ms ${easing} ${String(this.delay)}ms`;          return {            opacity: state.opacity ?? 1,            transform: toTransform(state, this.perspective),            filter: state.blur !== undefined ? `blur(${String(state.blur)}px)` : "none",            transition,          };        }}      >        {this.children}      </div>    );  }}

Examples

Every preset gets its own Storybook story, each with a Loop switch and a Replay button — built from this registry's own Switch and Button — so you can toggle looping on/off and re-trigger the entrance on demand without touching code.

Open "Open in Storybook" above and browse the Tailwind/Motion (or PraxisCSS/Motion) sidebar entry for the same demo across all 28 presets.

About

Purely presentational + an IntersectionObserver — no Morphos equivalent. Transitions plain CSS opacity/transform/filter between from and to. With inView (the default), the animation starts when the element scrolls into the viewport; with inView={false} it starts immediately on mount.

28 built-in effect presets resolve to a from/to/easing triple, covering fade, slide, zoom, flip (2D squish via a single-element perspective(), no wrapper needed), rotate, skew, blur, and bounce/elastic (overshoot easing, not multi-step keyframes) families. Direction suffixes describe where the element starts relative to its settled position"fade-up" starts below and rises into place, "fade-left" starts to the right and slides left into place, and so on. Explicit from/to/easing props always override whatever effect computes, so picking a preset never forecloses hand-tuning it further — pass effect="bounce-in" and duration={800} together, or drop effect entirely and specify from/to exactly as before.

Usage

import { Motion } from "@/components/ui/motion";

<Motion effect="fade-up" duration={600}>
  <Card>...</Card>
</Motion>

// or fully custom, same as before `effect` existed
<Motion from={{ opacity: 0, y: 16, rotate: -8 }} to={{ opacity: 1, y: 0, rotate: 0 }} duration={600}>
  <Card>...</Card>
</Motion>

Triggering it imperatively

Pass a trigger prop — a PraxisJS Command — to replay the animation on demand instead of (or in addition to) mount/scroll/loop. This is the same @Command()/@OnCommand() pattern used throughout PraxisJS for imperative, prop-driven actions. loop is reactive too, so a plain @State() boolean bound to a Switch can turn the ping-pong on and off at runtime — see the Storybook examples above for both wired up together.

import { StatefulComponent } from "@praxisjs/core";
import { Command, Component } from "@praxisjs/decorators";
import { Motion } from "@/components/ui/motion";

@Component()
class Demo extends StatefulComponent {
  @Command() replay!: Command;

  render() {
    return (
      <>
        <Motion trigger={this.replay} effect="bounce-in">
          <Card>...</Card>
        </Motion>
        <button onClick={() => this.replay.trigger()}>Replay</button>
      </>
    );
  }
}

Props

PropTypeDefault
effectMotionEffect (28 presets — see below)
from{ opacity?; x?; y?; scale?; scaleX?; scaleY?; rotate?; rotateX?; rotateY?; skewX?; skewY?; blur? }{ opacity: 0, y: 12 }, or the effect's preset
tosame shape as from{ opacity: 1, y: 0 }, or the effect's preset
easingstring (any CSS easing/cubic-bezier)"ease", or the effect's suggested easing
perspectivenumber (px, applied when rotateX/rotateY is set)800
durationnumber (ms)500
delaynumber (ms)0
oncebooleantrue
inViewbooleantrue
loopboolean (ping-pongs between from/to forever, ignoring once/inView; reactive — can be toggled after mount)false
triggerCommand (a @Command() field — calling .trigger() replays the animation)
classstring

MotionEffect presets

FamilyPresets
Fadefade, fade-up, fade-down, fade-left, fade-right
Slide (no opacity change)slide-up, slide-down, slide-left, slide-right
Zoomzoom-in, zoom-out
Flipflip-x, flip-y, flip-up, flip-down
Rotaterotate-in, rotate-left, rotate-right
Skewskew-up, skew-down
Blurblur-in
Bounce / elastic (overshoot easing)bounce-in, bounce-up, elastic-in, pop
Roll / droproll-in-left, roll-in-right, drop-in

On this page