plans/002-view-transition-target-resolution.md
animateView() non-root target resolution (View Transitions)Executor instructions: Follow this plan step by step. Run every verification command and confirm the expected result before moving to the next step. If anything in the "STOP conditions" section occurs, stop and report — do not improvise. When done, update the status row for this plan in
plans/README.md— unless a reviewer dispatched you and told you they maintain the index.Drift check (run first):
git diff --stat 42bfbe3ed..HEAD -- packages/motion-dom/src/view/If any in-scope file changed since this plan was written, compare the "Current state" excerpts against the live code before proceeding; on a mismatch, treat it as a STOP condition.
42bfbe3ed, 2026-06-10animateView() (View Transitions API integration) is public — it is exported from motion-dom/src/view and reaches the motion package through the export * chain — but its implementation carries three explicit TODOs that all describe the same gap: targets other than "root" are never resolved to elements, and elements are never auto-assigned view-transition-names. Today a user who writes animateView(update).get(".card", {...})-style non-root targets must manually set view-transition-name on every element in CSS for anything to happen, and selector/Element targets (the declared ViewTransitionTargetDefinition = string | Element type) are silently treated as pre-named layers. There is also zero E2E coverage: tests/ has animate/, animate-layout/, effects/, gestures/, scroll/ — no view/. Finishing this makes the already-shipped API actually deliver its typed surface.
Relevant files (all in packages/motion-dom/src/view/):
index.ts — ViewTransitionBuilder class and animateView() factory (lines 103–108). Targets are stored in targets = new Map<ViewTransitionTargetDefinition, ViewTransitionTarget>() (line 15).
types.ts — the target types:
// packages/motion-dom/src/view/types.ts:20
export type ViewTransitionTargetDefinition = string | Element
start.ts — the engine. The three TODOs:
// packages/motion-dom/src/view/start.ts:31
// TODO: Go over existing targets and ensure they all have ids
// packages/motion-dom/src/view/start.ts:60 (inside document.startViewTransition callback)
// TODO: Go over new targets and ensure they all have ids
// packages/motion-dom/src/view/start.ts:77-78 (inside targets.forEach)
// TODO: If target is not "root", resolve elements
// and iterate over each
Surrounding behavior you must preserve: when no "root" target exists, :root gets view-transition-name: none (start.ts:37–41); a CSS rule forces animation-timing-function: linear !important on all view-transition pseudo-elements so easing can be applied via updateTiming (start.ts:50–53); per-target animations are built by iterating targets and matching generated getViewAnimations() entries.
utils/get-layer-info.ts, utils/get-view-animations.ts — how generated WAAPI view-transition animations are discovered and matched to layer names. Read both before designing (Step 1).
packages/motion-dom/src/utils/resolve-elements.ts — existing resolveElements(ElementOrSelector) helper; use it for selector resolution (it is what LayoutAnimationBuilder uses).
Export chain: view/index.ts → motion-dom/src/index.ts:154 → framer-motion/src/dom.ts:1 (export * from "motion-dom") → motion package. No export wiring needed.
Repo conventions: named exports, interface for type definitions, optional chaining over if, small output size is a priority.
| Purpose | Command (from repo root) | Expected on success |
|---|---|---|
| Build | yarn build | exit 0 |
| motion-dom unit tests | npx jest --config packages/motion-dom/jest.config.json --testPathPattern="view" | all pass |
Playwright (real Chromium — required, JSDOM/Electron lack startViewTransition) | npx playwright test tests/view/ | all pass |
| Lint | yarn lint | exit 0 |
Playwright config: playwright.config.ts — testDir: "./tests", baseURL: http://localhost:8000/playwright/, auto-started webServer. Test pages live in dev/html/public/playwright/ and specs in tests/<area>/<name>.spec.ts. After editing motion-dom source, rebuild before running E2E (fixtures consume built output).
In scope:
packages/motion-dom/src/view/start.tspackages/motion-dom/src/view/index.ts (only if target bookkeeping needs a normalized key)packages/motion-dom/src/view/utils/ (new helper file allowed, e.g. assign-names.ts)packages/motion-dom/src/view/__tests__/ (create unit tests)dev/html/public/playwright/view-*.html (create fixture pages)tests/view/ (create Playwright specs)Out of scope:
getViewAnimations / browser-global typings in types.global.ts unless a type error forces a minimal addition.linear !important mechanism — required by the easing strategy; do not "simplify" it.advisor/002-view-target-resolutionRead start.ts in full plus utils/get-layer-info.ts and utils/get-view-animations.ts. Answer in a short note committed as plans/002-notes.md:
builder.targets (a string | Element) currently end up matched against a generated animation's layer name? (Trace the targets.forEach body in start.ts:75 onward.)Element target? Proposal to validate: selectors that look like pre-named layers (plain identifiers, e.g. "card") keep current behavior; CSS selectors / Elements get resolved via resolveElements and each element receives a generated view-transition-name (e.g. «motion-view-N» counter) set as an inline style before capture and removed in transition.finished.finally, alongside the existing css.remove() cleanup.document.startViewTransition (TODO at line 31), and elements created by update() named inside the transition callback after await update() (TODO at line 60). Decide how re-resolution after update() works (re-run the selector).Verify: plans/002-notes.md exists and answers all three questions with file:line citations.
Implement per your validated design. Required behaviors:
"root" keeps its special-casing untouched.resolveElements, assign generated names (skip elements that already have a view-transition-name inline or computed), build one animation set per resolved element by iterating, and clean up generated names in finished.finally.Verify: yarn build → exit 0.
Create packages/motion-dom/src/view/__tests__/resolve-targets.test.ts for the pure parts (name generation, skip-if-already-named, cleanup bookkeeping). JSDOM has no startViewTransition; note start.ts:24–29 already has a fallback branch (calls update(), resolves an empty GroupAnimation) — unit-test that fallback still works with element targets.
Verify: npx jest --config packages/motion-dom/jest.config.json --testPathPattern="view" → all pass.
Create dev/html/public/playwright/view-target-element.html and view-target-selector-multiple.html fixtures plus tests/view/view-targets.spec.ts:
animateView with an Element target animating opacity on its new layer → assert the element visibly transitions (sample computed style mid-transition or assert via document.getAnimations() that a ::view-transition-* animation for the generated name exists).view-transition-name behaves as before.Model spec structure after an existing file in tests/animate-layout/animate-layout.spec.ts. Chromium only — skip in browsers without document.startViewTransition (guard with a feature check + test.skip).
Verify: npx playwright test tests/view/ → all pass.
Delete the three TODO comments (now implemented). Run full gates.
Verify: grep -n "TODO" packages/motion-dom/src/view/start.ts → no matches; npx jest --config packages/motion-dom/jest.config.json → no new failures; yarn lint → exit 0.
view/__tests__/resolve-targets.test.ts — name generation, already-named skip, no-startViewTransition fallback.tests/view/view-targets.spec.ts — the three cases in Step 4 (element target, multi-element selector, pre-named regression).tests/animate-layout/animate-layout.spec.ts.yarn build exits 0grep -n "TODO" packages/motion-dom/src/view/start.ts → no matchesnpx jest --config packages/motion-dom/jest.config.json --testPathPattern="view" → all passnpx playwright test tests/view/ → all pass, including the pre-named-layer regression casegit status)plans/README.md status row updatedStop and report back (do not improvise) if:
getViewAnimations's matching contract beyond reading layer names.document.startViewTransition is unavailable in the repo's pinned Playwright Chromium (check with a trivial probe first) — report; do not swap test frameworks.animateView) will build on exactly this resolution layer; keep the element→name assignment in its own helper so React can reuse it.view-transition-names must not leak after interrupted transitions (interrupt: "immediate" path in view/queue.ts / ViewTransitionOptions).animateView on motion.dev (docs live outside this repo).