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 motionpnpm dlx kosmesis add motionyarn dlx kosmesis add motionbunx kosmesis add motionCopy and paste the following code into your project.
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/csspnpm add @praxisjs/cssyarn add @praxisjs/cssbun add @praxisjs/cssCopy and paste the following code into your project.
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
| Prop | Type | Default |
|---|---|---|
effect | MotionEffect (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 |
to | same shape as from | { opacity: 1, y: 0 }, or the effect's preset |
easing | string (any CSS easing/cubic-bezier) | "ease", or the effect's suggested easing |
perspective | number (px, applied when rotateX/rotateY is set) | 800 |
duration | number (ms) | 500 |
delay | number (ms) | 0 |
once | boolean | true |
inView | boolean | true |
loop | boolean (ping-pongs between from/to forever, ignoring once/inView; reactive — can be toggled after mount) | false |
trigger | Command (a @Command() field — calling .trigger() replays the animation) | — |
class | string | — |
MotionEffect presets
| Family | Presets |
|---|---|
| Fade | fade, fade-up, fade-down, fade-left, fade-right |
| Slide (no opacity change) | slide-up, slide-down, slide-left, slide-right |
| Zoom | zoom-in, zoom-out |
| Flip | flip-x, flip-y, flip-up, flip-down |
| Rotate | rotate-in, rotate-left, rotate-right |
| Skew | skew-up, skew-down |
| Blur | blur-in |
| Bounce / elastic (overshoot easing) | bounce-in, bounce-up, elastic-in, pop |
| Roll / drop | roll-in-left, roll-in-right, drop-in |