npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wcstack/view-transition

v4.20.0

Published

Declarative View Transition arbiter for Web Components. Framework-agnostic policy tag that animates @wcstack/router route swaps and @wcstack/state list/branch updates via the transition-runner protocol.

Readme

@wcstack/view-transition

🤖 AI coding agents: This README is a package-level reference, not the primary entry point for building a wcstack application. If you have not already done so, first read the repository README and AGENTS.md, then use the wcstack-app skill.

@wcstack/view-transition gives a wcstack page leave and move animations — the two things CSS alone cannot do for DOM that a framework removes.

日本語版: README.ja.md

<script type="module" src="https://esm.run/@wcstack/view-transition/auto"></script>

<wcs-view-transition></wcs-view-transition>

That is the whole opt-in. From then on, route swaps by @wcstack/router and list / branch updates by @wcstack/state run inside a View Transition, and you style them in CSS against ::view-transition-*.

<wcs-view-transition> is a policy node, not an I/O node: it renders nothing, binds no data, and describes no animation. It decides whether a DOM change animates and what happens when two of them collide. The animation itself stays in CSS, where it belongs.

  • inputs: for, mode, naming, naming-limit, reduced-motion, types, disabled
  • outputs: active, error
  • commands: skip()

What you already had without this package

Worth knowing before you install anything, because two thirds of the problem never needed a package:

/* Enter: a newly inserted row / branch animates in, no JS involved. */
li {
  transition: opacity 0.2s, transform 0.2s;
  @starting-style { opacity: 0; transform: translateY(-4px); }
}

class.x: and style.y: bindings write to the live element, so ordinary CSS transitions apply to any value change. @starting-style covers entering elements — which is exactly what a new for row and a mounting if branch are.

What CSS cannot reach is leaving: wcstack detaches removed nodes synchronously, so by the next paint there is nothing left to animate. And a reorder is a series of insertBefore calls with no intermediate state, so a move cannot be tweened either. That is what this package is for — the browser snapshots the old state before the change, so a leaving element does not have to survive it.

Install

npm install @wcstack/view-transition

Quick start

Route transitions only

<wcs-view-transition for="router"></wcs-view-transition>

<style>
  ::view-transition-old(root) { animation: fade-out 0.2s both; }
  ::view-transition-new(root) { animation: fade-in 0.2s both; }
</style>

for="router" keeps @wcstack/state's update timing untouched (see Contract).

List rows that move and fade

<wcs-view-transition naming="auto"></wcs-view-transition>

<ul>
  <template data-wcs="for: todos">
    <li>{{ .title }}</li>
  </template>
</ul>

<style>
  /* every auto-named row shares the wcs-row group class */
  ::view-transition-group(*.wcs-row) { animation-duration: 0.25s; }
  ::view-transition-old(*.wcs-row) { animation: fade-out 0.25s both; }
  ::view-transition-new(*.wcs-row) { animation: fade-in 0.25s both; }
</style>

Attributes

| Attribute | Values | Default | Meaning | |---|---|---|---| | for | router, state (space-separated) | router state | Which participants animate. | | mode | latest / queue / exhaust | latest | What happens when a change arrives while a transition is running — see Exclusion. | | naming | manual / auto | manual | Who assigns view-transition-name — see Naming. | | naming-limit | integer | 200 | Cap on auto-assigned names. | | reduced-motion | skip / animate | skip | skip honors prefers-reduced-motion: reduce by applying changes without animating. | | types | space-separated | — | Passed to startViewTransition({ types }) where supported, for :active-view-transition-type(). | | disabled | boolean | absent | Inert: every change applies immediately, no transitions. Bindable, so a page can switch animation off from state. |

Each attribute is also a property — for, mode, naming, naming-limit, reduced-motion, types and disabled are participants, mode, naming, namingLimit, reducedMotion, types and disabled — and writing the property writes the attribute, so the two never disagree and a value set from JS survives moving the element. types and participants take an array or a space-separated string and are written as space-separated tokens; the getters return the parsed value (an array, a number, a known keyword).

null and undefined. All seven inputs take null as "clear": the attribute is removed and the input falls back to its default in the table (namingLimit = null is the default 200, not a limit of 0). undefined means "no value supplied": the attribute goes back to what it was before the first write — the value written in the markup, or none. Neither is ever written as the string "null" / "undefined". (@wcstack/state never writes undefined; React 19 does when a prop that had a value is removed, and so can a direct assignment.) disabled is on only for true.

One <wcs-view-transition> per document. A second one warns and stays inert rather than fighting the first over the exclusion it exists to provide.

Contract

The rule everything else is subordinate to: a DOM change is applied exactly once, whatever happens to its animation. An unsupported browser, a hidden tab, a page not drawn yet, prefers-reduced-motion, disabled, a collision, or a startViewTransition that throws all still end with the change applied. The page is never left showing stale DOM because an animation could not play.

What is worth knowing before you add the tag:

  1. for="state" (on by default) makes the state drain asynchronous. Today's drain lands on a microtask; inside a transition it lands on a frame. Code that writes state and then reads the DOM after await Promise.resolve() needs to wait for the transition instead. $renderedCallback is unaffected — it still fires right after the bindings are applied — and since 4.10 so are $watch and the $stream restart: they wait for the bindings the transition applies, so a $watch that navigates or moves the focus sees the page the update produced, and what it writes is applied in the same transition (up to 4.9 they ran first: a login that navigated from a $watch ran before <wcs-storage> saved the session, and a guard reading it sent the reader back). Use for="router" to keep the drain exactly as it was.
  2. Participation is per document, not per element. One updater drains every <wcs-state> on the page, so for="state" turns transitions on for all of them.
  3. Only a change of structure animates. A state update that adds, removes or moves list rows, or switches an if branch, runs in a transition; one that only updates text, properties or classes is applied at once, as without the tag (a value change is CSS transitions' job). Typing in a bound input therefore never starts a transition — it used to start one per keystroke, and the page did not take a click while it ran.
  4. The router's path comes a frame after its other outputs. The router sends routeName, params, searchParams and data before it places the route's content, and path — "the navigation has finished" — after it; with the tag the content is placed in the transition's update callback, a frame later. Code that runs in that frame for another reason (a response, a timer) and needs where the reader is now reads routeName / params / searchParams or location, not path (router README, Firing contract).

Transitions are skipped — and the change applied synchronously, on exactly today's timing — when the browser has no startViewTransition, when document.hidden is true (a background tab gets no rendering opportunities, so a transition there would freeze the DOM until you look at the tab again), before the page has drawn its first frame (since 4.10.1: there is nothing on screen to transition from, and Chromium held that first frame — static text included — until the update callback ran, so data fetched on load kept the page blank for 0.2–2.5 s), under prefers-reduced-motion: reduce unless reduced-motion="animate", while disabled, and during SSR. The page has drawn once a Paint Timing entry exists or a frame has passed since the tag was connected; a browser that has just started may take a while to draw its first page (Playwright's headless shell, over 2 s), and a test that counts transitions waits for a frame first (requestAnimationFrame then setTimeout).

Exclusion

Transitions cannot nest, so something has to arbitrate. Every request made in the same microtask joins one transition (a route change and the state drain it triggers animate together rather than cancelling each other), and mode decides what a later collision does:

| mode | While a transition is running | |---|---| | latest | Skip the running one, animate the newcomer. The default. | | queue | Chain: the newcomer starts once the running one has finished. | | exhaust | Apply the newcomer's change immediately, without animating it. |

exhaust drops the animation, never the DOM update. And in all three modes, a request that arrives before the running transition has taken its snapshot joins that transition, so changes are never applied out of order.

skip() finishes the running transition immediately; the DOM change it carries still happens.

Naming

view-transition-name has to be on an element before the browser snapshots it, so it cannot be assigned while the change is being made. "Name only what changed" is therefore impossible, and you pick one of two strategies:

manual (default) — you bind it, and only what should morph gets a name:

<template data-wcs="for: todos">
  <li data-wcs="style.viewTransitionName: .cssName">{{ .title }}</li>
</template>

auto — @wcstack/state names the first element of every list row and if branch as it mounts, and adds a view-transition-class (wcs-row / wcs-branch) so CSS can address the whole group. The name follows the content, so a pooled row reused for another item keeps its name — which is what the DOM actually did.

Auto naming is capped at naming-limit (200 by default) because every named element becomes its own snapshot group and a few hundred of them make a transition visibly slow. Past the cap naming stops and says so once in the console. Big lists should use manual and name deliberately.

auto is load-order sensitive. Names are assigned as content mounts, so rows and branches that were already on the page when this tag upgraded never get one — nothing revisits them. Put this package's script tag before the @wcstack/state one so the arbiter is installed first; otherwise the first render participates in the root snapshot rather than morphing row by row. manual has no such ordering constraint, because the name is an ordinary binding.

Binding it from state

<wcs-view-transition
  data-wcs="disabled: animationsOff; active: transitionRunning"
></wcs-view-transition>

active tracks whether a transition is running; error carries the last failure to start one (both observable). disabled, mode, naming, types and participants are writable inputs, and skip is available as a command token.

Direct use (no DOM)

import { ViewTransitionCore } from "@wcstack/view-transition";

const core = new ViewTransitionCore();
core.naming = "auto";
core.install();               // become the page's arbiter
await core.run(() => { /* mutate the DOM */ });

install() publishes the core on a well-known global symbol; @wcstack/state and @wcstack/router find it there without importing this package. The protocol is described in docs/view-transition-design.md §4, and getTransitionRunner / runTransition / TRANSITION_RUNNER_KEY are exported for adopters who want to install an arbiter of their own.

Demo

examples/list-transitions — enter, leave and move in one page, with a checkbox that switches the arbiter off so the difference is visible. Buildless: open index.html.

Browser support

Same-document View Transitions ship in Chromium 111+, Safari 18+ and Firefox 144+. In an older browser, and anywhere else the API is missing, every change applies immediately with no animation and no error — the page keeps working, it just does not animate.

License

MIT