Fit Animation

Morph one element's position and size to match another using FLIP-style animations.

Fit animation morphs one element’s geometry — position and size — to match another element’s. The SDK measures both elements’ bounding rects at play time and animates the visual delta between them. When fit is set, from and to are ignored.

Basic Usage

Add a fit object to your animation config. Set target to a CSS selector pointing to the element you want to morph toward.

typescript
import { Motion } from "@motion.page/sdk";

Motion("morph", ".card", {
  fit: { target: ".modal" },
  duration: 0.6,
  ease: "power3.inOut",
}).onClick({ toggle: "reverse" });

The source element (.card) animates toward the position and size of the target element (.modal). Geometry is captured at play time using getBoundingClientRect(), so the page can reflow between setup and play without stale measurements.


fit Options

OptionTypeDefaultDescription
targetstringRequired. CSS selector for the element to morph toward. Measured at play time.
scalebooleantrueInclude scaleX/scaleY transforms. Disable for position-only movement.
resizebooleanfalseAnimate width/height instead of scale. No content distortion. Mutually exclusive with scale.
absolutebooleanfalseAnimate CSS left/top/width/height. Sets position: absolute on the source element.

Animation Modes

Fit supports three modes depending on which CSS properties you want animated.

Transform Mode (default)

Animates x, y, scaleX, and scaleY CSS transforms. GPU-accelerated and the most performant option. The trade-off: scaling can visually distort text, borders, and box-shadows at intermediate frames.

typescript
Motion("morph", ".card", {
  fit: { target: ".modal" },
  duration: 0.6,
  ease: "power3.inOut",
}).onClick({ toggle: "reverse" });

Resize Mode

Set resize: true to animate x, y, width, and height instead of scale transforms. The element physically changes size — no distortion at any point — but it triggers layout reflow on every frame. Mutually exclusive with scale.

typescript
Motion("expand", ".card", {
  fit: { target: ".modal", resize: true },
  duration: 0.5,
  ease: "power2.inOut",
}).onClick({ toggle: "reverse" });

Use resize mode for text-heavy cards or elements with prominent borders where scale distortion would be noticeable.

Absolute Mode

Set absolute: true to animate CSS left, top, width, and height. The SDK sets position: absolute on the source element before animating. This pulls the element out of document flow, preventing it from pushing surrounding content during the animation.

typescript
Motion("layout", ".panel", {
  fit: { target: ".expanded", absolute: true },
  duration: 0.5,
  ease: "power2.inOut",
}).onClick({ toggle: "reverse" });

Note: In the current version, absolute: true permanently sets position: absolute on the element — it is not reverted after the animation completes.


Position Only (No Size Change)

Set scale: false to animate position without changing size. The source element slides to the target’s center with no scaling.

typescript
Motion("slide", ".indicator", {
  fit: { target: ".active-tab", scale: false },
  duration: 0.3,
  ease: "power2.out",
}).onClick({ toggle: "reverse" });

Common Patterns

Tab Indicator

A classic active-state indicator that slides and resizes to match the active tab.

typescript
const tabs = document.querySelectorAll(".tab");

tabs.forEach((tab) => {
  tab.addEventListener("click", () => {
    Motion("tab-indicator", ".indicator", {
      fit: { target: tab as HTMLElement, resize: true },
      duration: 0.35,
      ease: "power3.inOut",
    }).play();
  });
});

Card Expand to Modal

A card that morphs into a full-screen overlay on click. Use resize: true to avoid distorting the card’s content at intermediate frames.

typescript
Motion("card-expand", ".card-thumbnail", {
  fit: { target: ".card-fullscreen", resize: true },
  duration: 0.5,
  ease: "power2.inOut",
}).onClick({ target: ".card-thumbnail" });

Grid Reorder with Stagger

Animate multiple grid items toward their new slot positions with a stagger.

typescript
Motion("grid-reorder", ".grid-item", {
  fit: { target: ".grid-slot" },
  stagger: 0.08,
  duration: 0.5,
  ease: "power2.inOut",
}).onClick({ target: ".reorder-btn", toggle: "reverse" });

Shared Element Transition

Simulate a shared-element transition between two views — the small thumbnail morphs toward the large detail view.

typescript
// Trigger when navigating to the detail view
Motion("shared-element", ".thumbnail", {
  fit: { target: ".detail-hero", resize: true },
  duration: 0.6,
  ease: "expo.inOut",
}).play();

Limitations

  • SVG not supportedfit only works with HTML elements. Passing an SVG element as source or target logs a [Motion Fit] warning and the animation is skipped.
  • from and to are ignored — when fit is set, any from or to properties in the config are discarded. Use a multi-step timeline if you need to combine fit with other property changes.
  • resize and scale are mutually exclusive — setting both has no defined behavior. Use one or the other.
  • absolute side effectabsolute: true permanently changes position to absolute on the source element, which can affect surrounding layout.

API Reference

typescript
interface FitConfig {
  /** CSS selector for the target element to morph toward. Measured at play time via getBoundingClientRect(). */
  target: string;
  /** Include scaleX/scaleY transforms. Default: true. May distort text, borders, and shadows. */
  scale?: boolean;
  /** Animate width/height instead of scale. No distortion, but triggers layout reflow. Mutually exclusive with scale. Default: false. */
  resize?: boolean;
  /** Use position: absolute during animation. Animates left/top/width/height. Prevents layout shifts. Default: false. */
  absolute?: boolean;
}

interface AnimationConfig {
  // When set, `from` and `to` are ignored
  fit?: FitConfig;
}

Related: Scale · Translate · Dimensions · Stagger