Advanced Targeting

Target specific pages with post types, custom RegEx URL patterns, and selector strategies.

Targeting controls which elements animate and which pages an animation runs on. This page covers compound CSS selectors, scoped targeting, dynamic elements, URL-based page matching, and WordPress post type targeting.


CSS Selector Strategies

The SDK accepts any valid CSS selector string as a TargetInput. Complex selectors let you target elements precisely without adding extra classes to your HTML.

Compound Selectors

Combine multiple selector rules to narrow the target:

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

// Elements that have BOTH classes
Motion("combined", ".card.featured", {
  from: { opacity: 0, y: 30 },
  duration: 0.6,
}).onPageLoad();

// Direct children only
Motion("children", ".menu > .menu-item", {
  from: { x: -20, opacity: 0 },
  stagger: 0.06,
  duration: 0.4,
}).onPageLoad();

// Adjacent sibling
Motion("sibling", ".hero + .intro", {
  from: { opacity: 0 },
  duration: 0.8,
}).onPageLoad();

:nth-child and Structural Selectors

Target elements by their position in the DOM:

typescript
// Every other card — skip the first
Motion("even-cards", ".grid .card:nth-child(even)", {
  from: { y: 40, opacity: 0 },
  duration: 0.5,
  stagger: 0.07,
}).onScroll({ each: true });

// First three items only
Motion("first-three", ".list-item:nth-child(-n+3)", {
  from: { opacity: 0, x: -20 },
  duration: 0.5,
  stagger: 0.08,
}).onPageLoad();

// Last item in a group
Motion("last-item", ".nav-item:last-child", {
  to: { color: "#9966FF" },
  duration: 0.3,
}).onHover({ onLeave: "reverse" });

Data Attribute Selectors

Target elements by their HTML data attributes — useful when you control the markup but not the class names:

typescript
// Any element with the attribute present
Motion("data-reveal", "[data-animate]", {
  from: { opacity: 0, y: 20 },
  duration: 0.5,
  stagger: 0.06,
}).onScroll({ each: true });

// Attribute with a specific value
Motion("data-hero", "[data-role='hero']", {
  from: { scale: 0.95, opacity: 0 },
  duration: 0.8,
  ease: "power3.out",
}).onPageLoad();

// Attribute starting with a prefix — useful for BEM modifiers
Motion("data-prefix", "[data-speed^='fast']", {
  from: { y: 30, opacity: 0 },
  duration: 0.3,
}).onPageLoad();

Selector Arrays

Pass multiple selectors to animate them as one group:

typescript
// All three animate together as a single timeline
Motion("multi", ["h1", ".subtitle", ".cta-button"], {
  from: { opacity: 0, y: 30 },
  duration: 0.6,
  stagger: 0.1,
}).onPageLoad();

Scoped Targeting

Motion.utils.toArray(target, scope) resolves a selector only within a specific container. Use this when your page has repeated components with identical class names and you need to target only the one inside a certain parent.

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

// Resolve .card only inside #featured-section
const cards = Motion.utils.toArray(".card", "#featured-section");

Motion("scoped-cards", cards, {
  from: { opacity: 0, y: 30 },
  duration: 0.6,
  stagger: 0.08,
}).onPageLoad();

This prevents animations from leaking into other sections that share the same class names.

Scoping with Refs (React)

In React, pass the ref element as the scope to avoid stale global selectors:

tsx
import { useEffect, useRef } from "react";
import { Motion } from "@motion.page/sdk";

export function ProductGrid() {
  const gridRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!gridRef.current) return;

    const cards = Motion.utils.toArray(".card", gridRef.current);

    Motion("grid-reveal", cards, {
      from: { opacity: 0, y: 24 },
      duration: 0.5,
      stagger: 0.07,
      ease: "power2.out",
    }).onScroll({ each: true });

    return () => Motion.kill("grid-reveal");
  }, []);

  return (
    <div ref={gridRef} className="product-grid">
      {/* cards rendered here */}
    </div>
  );
}

Dynamic Element Targeting

Elements added after the page loads — via AJAX, pagination, or framework rendering — won’t be found by selectors that ran at init time. Use Motion.context() to scope animations so they can be reinitialized cleanly when the DOM changes.

Motion.context() for Reinit

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

let ctx = Motion.context(() => {
  Motion("cards", ".card", {
    from: { opacity: 0, y: 30 },
    duration: 0.5,
    stagger: 0.07,
  }).onScroll({ each: true });
});

// After AJAX inserts new .card elements:
ctx.refresh(); // kills old animations, re-runs init, re-resolves .card

ctx.refresh() destroys the previous timelines and re-runs the factory function. All selectors are evaluated again against the updated DOM.

MutationObserver Pattern

Use a MutationObserver to detect when new elements appear and reinitialize:

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

let ctx = Motion.context(() => {
  Motion("dynamic-items", "[data-animate]", {
    from: { opacity: 0, y: 20 },
    duration: 0.4,
    stagger: 0.06,
  }).onPageLoad();
});

const observer = new MutationObserver(() => {
  ctx.refresh();
});

// Watch for added nodes anywhere in the list container
observer.observe(document.querySelector("#results-list")!, {
  childList: true,
  subtree: true,
});

// Clean up when the page unloads
window.addEventListener("pagehide", () => {
  observer.disconnect();
  ctx.revert();
});

Performance tip: Debounce the observer callback if the DOM updates frequently in rapid bursts. Call ctx.refresh() once after the burst settles rather than on every mutation.

Debounced Observer

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

let ctx = Motion.context(() => {
  Motion("feed-items", ".feed-item", {
    from: { opacity: 0, y: 16 },
    duration: 0.35,
    stagger: 0.05,
  }).onPageLoad();
});

let debounceTimer: ReturnType<typeof setTimeout>;

const observer = new MutationObserver(() => {
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(() => ctx.refresh(), 150);
});

observer.observe(document.querySelector("#feed")!, {
  childList: true,
  subtree: true,
});

See SPA Integration for framework-specific lifecycle patterns.


Page-Specific Targeting (URL Matching)

Run an animation only on pages whose URL matches a pattern. Check window.location before registering the timeline.

Exact Path Match

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

if (window.location.pathname === "/about") {
  Motion("about-hero", ".hero-title", {
    from: { opacity: 0, y: 40 },
    duration: 0.8,
    ease: "power3.out",
  }).onPageLoad();
}

RegEx URL Patterns

Use RegExp.test() to match URL patterns — slug fragments, query strings, or path segments:

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

// Any URL containing "blog"
if (/blog/.test(window.location.pathname)) {
  Motion("blog-reveal", ".post-card", {
    from: { opacity: 0, y: 24 },
    duration: 0.5,
    stagger: 0.08,
  }).onScroll({ each: true });
}

// Case-insensitive match — product pages
if (/\/products?\//i.test(window.location.pathname)) {
  Motion("product-fade", ".product-image", {
    from: { scale: 0.97, opacity: 0 },
    duration: 0.6,
    ease: "power2.out",
  }).onScroll({ each: true });
}

// Homepage only (root path)
if (/^\/$/.test(window.location.pathname)) {
  Motion("home-hero", "#hero", {
    from: { opacity: 0, y: 60 },
    duration: 1,
    ease: "power3.out",
  }).onPageLoad();
}

Multiple Page Conditions

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

const path = window.location.pathname;
const isLanding = /^\/(home|landing|index)/.test(path);
const isProduct = /\/product\//.test(path);

if (isLanding || isProduct) {
  Motion("conversion-cta", ".cta-block", {
    from: { opacity: 0, scale: 0.96 },
    duration: 0.7,
    ease: "back.out",
  }).onScroll({ toggleActions: "play none none none" });
}

WordPress: Post Type Targeting

In the Motion.page Builder, the Advanced Targeting panel lets you restrict a timeline to specific WordPress post types, template pages, or URL patterns — without writing any code.

How It Works

Open the Advanced Targeting panel in the Left Panel under the timeline name. Use the dropdown to select:

OptionWhat it matches
Post type slug (e.g. post, page, product)All posts of that type
$searchThe WordPress search results page
$404The 404 error page
Custom RegEx stringAny URL matching the pattern

Selected post types are stored per timeline and evaluated at runtime — the animation only loads on matching pages.

RegEx in the Builder

The Advanced Targeting input also accepts custom RegEx strings. Enter the pattern without delimiters — the Builder wraps it automatically:

InputMatches
aboutAny URL containing “about”
\/shop\/.*\/reviewURLs like /shop/product-name/review
\?ref=emailURLs with the ?ref=email query parameter

Rules:

  • Delimiters (/…/) are added automatically — do not include them.
  • The only supported flag is i (case-insensitive). Add it manually if needed.
  • Escape special regex characters with \ — e.g. use \. to match a literal dot.

Example: To match all pages with the slug services or service, enter services? — the ? makes the trailing s optional.

WordPress Code Equivalent

If you’re using the SDK directly in a WordPress theme, replicate the same logic with window.location:

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

// Equivalent to targeting the "product" post type
// WordPress adds the slug in the URL — match accordingly
if (/\/product\//.test(window.location.pathname)) {
  Motion("product-reveal", ".entry-content", {
    from: { opacity: 0, y: 20 },
    duration: 0.6,
    ease: "power2.out",
  }).onPageLoad();
}

For the full WordPress plugin targeting workflow, see the Builder documentation.


Tips and Gotchas

Selectors resolve at registration time. If elements don’t exist yet when Motion() is called, the timeline targets zero elements. Use Motion.context() with ctx.refresh() for late-rendered content.

each: true is per-element, not per-page. The each option on triggers creates independent timeline instances for each matched element. It is separate from page-level URL targeting.

Compound selectors with stagger. When using stagger across a compound selector, all matched elements animate in DOM order regardless of which sub-selector found them:

typescript
// All .card and .featured-card elements, staggered together in DOM order
Motion("mixed", [".card", ".featured-card"], {
  from: { opacity: 0, y: 20 },
  duration: 0.5,
  stagger: 0.06,
}).onPageLoad();

Scoping prevents bleed. Always use Motion.utils.toArray(selector, scope) when the same component renders multiple times on one page. Without a scope, .card matches every .card on the page.