esc
The View Transition Strikes Back
The V3 Saga

The View Transition Strikes Back

Previously on The V3 Saga The Architecture had awakened. A 1000-line document proclaimed the future. Mobile-style navigation. Orthogonal templates. Signal hierarchy. The Lizard Brain had nodded...

December 9, 2025

Previously on The V3 Saga

The Architecture had awakened. A 1000-line document proclaimed the future. Mobile-style navigation. Orthogonal templates. Signal hierarchy. The Lizard Brain had nodded approvingly.

But at the end of that document, buried in the CSS examples, lay a prophecy:

::view-transition-old(slide-forward) { animation: slide-out-left 0.2s; }
::view-transition-new(slide-forward) { animation: slide-in-right 0.2s; }

“Next episode,” it promised, “theory becomes practice.”

The Lizard Brain should have been suspicious.

The Experiment Begins

Afternoon — “Let’s test view transitions on the credential form path.”

Simple enough. Security → Credentials → Edit Credential. Three screens. Push, push, pop, pop. Mobile-style slides. The architecture document had diagrams.

First attempt: CSS animations with .nav-in class on morphed content.

Nothing happened.

“Of course,” mutters the developer. “Datastar morphs elements. Same DOM node. CSS animations need element replacement.”

The Squirrel perks up. “Let’s read Datastar’s source code!”

Into the Morph

15 minutes later — Deep in morph.ts. ID-based morphing. Elements with same ID are updated in place, not replaced. The old element IS the new element. No animation trigger.

“But Datastar has view transitions!” The Squirrel is optimistic. “There’s a __viewtransition modifier on data-on:click!”

Try it. Click. FLASH. The whole screen blinks white then settles.

“That’s… not what mobile apps do.”

The SDK Discovery

The Lizard Brain awakens from a nap. “Aren’t we using the Go SDK? PatchElements? What options does it have?”

sse.PatchElements(buf.String(), datastar.WithUseViewTransitions(true))

Update the handlers. Refresh. Click.

Crossfade.

“It’s… animating! The old content fades out, new fades in!”

The Squirrel is ecstatic. “Now we just need bidirectional! Push slides left, pop slides right! Like the architecture document says!”

The Lizard Brain’s eye twitches.

The Rabbit Hole Opens

The plan: Use a signal. $sidebarDirection. Set it to 'push' or 'pop' before navigation. CSS reads the signal via data-view-transition. Different animations play.

Attempt 1: Put data-view-transition="$sidebarDirection" on the clicked element.

Nothing. The View Transitions API captures state before the click handler runs.

Attempt 2: Set direction in click handler, then @get.

data-on:click="$sidebarDirection = 'push'; @get('/credentials/new')"

Still crossfade. The signal changes, but… timing?

Attempt 3: Maybe the attribute needs to be on the element being transitioned?

Move data-view-transition to #sidebar-content.

Crossfade.

Attempt 4: Check if the Pro bundle is even loaded. Grep the minified JS. Yes, view-transition attribute is there.

Attempt 5: Slow it down to 2 seconds to actually SEE what’s happening.

“Interesting. It slides OUT correctly. But fades IN.”

The Squirrel’s ears droop.

The Morphing Problem

The revelation: When Datastar morphs #sidebar-content, it REPLACES the element. The new element from the server doesn’t have data-view-transition on it yet.

Old element: has view-transition-name: push (from signal)
New element: no view-transition-name (attribute not processed yet)

View Transitions API captures “old” state, applies DOM changes, captures “new” state. But “new” doesn’t have the name. They don’t match. Default crossfade.

Attempt 6: Add data-view-transition="$sidebarDirection" to EVERY template’s #sidebar-content.

<div class="sidebar__content" id="sidebar-content" data-view-transition="$sidebarDirection">

Credential list. Credential form. Security page. Dashboard pages. ALL of them.

Test.

Still crossfade.

Screenshot DevTools. Look at computed styles.

view-transition-name is… NOT IN THE LIST.

The attribute is on the element. The signal has a value. The Pro JS is loaded. But the style isn’t being set.

The Squirrel’s Last Stand

“Maybe we need to—”

“Stop.”

The Lizard Brain speaks.

“Crossfade is beautiful.”

“But the architecture document—”

“The architecture document was THEORY. This is PRACTICE. We’ve spent hours on bidirectional slides. The crossfade already works. It’s smooth. It’s professional. It’s reliable.”

“But mobile apps—”

“The Swift version can have slides. This is a web app. Crossfade is more than enough.”

Silence.

The Great Simplification

Delete the slide keyframes. Delete the direction signal. Delete the data-view-transition attributes from templates. Delete $sidebarDirection from click handlers.

What remains:

#sidebar-content {
  view-transition-name: sidebar-content;
}

That’s it. 18 lines of CSS total. Server sends useViewTransition: true. Browser does a smooth crossfade. Done.

The commit message: “refactor(v3): Simplify view transitions to crossfade only”

5 files changed. 15 insertions. 102 deletions.

The Lessons

Lesson 1: Theory is not practice. A beautiful architecture diagram doesn’t mean the browser will cooperate.

Lesson 2: The View Transitions API is powerful but timing-sensitive. When you morph elements, the “new” state needs the view-transition-name BEFORE the snapshot is taken. Reactive attributes that set styles AFTER hydration don’t work.

Lesson 3: Crossfade is not a consolation prize. It’s a legitimate, beautiful transition that works reliably.

Lesson 4: Know when to stop. The rabbit hole has no bottom. At some point, “good enough” IS the right answer.

Lesson 5: setTimeout is a code smell. If your solution requires arbitrary delays, your solution is wrong.

The Tally

Time spent:              ~3 hours
Attempts at bidirectional: 6+
Lines of CSS deleted:    ~90
Lines of CSS remaining:  18
View transition working: YES (crossfade)
Bidirectional slides:    NOPE (and that's fine)
Rabbit holes explored:   1 (deep)
Lessons learned:         5
Lizard Brain vindicated: again

The Moral

Sometimes the architecture document is aspirational. Sometimes the browser has other plans. Sometimes the Squirrel needs to be told “no.”

And sometimes, crossfade is beautiful enough.

🦎


Next episode: TBD — In which something else seems simple until it isn’t.


See also:

The V3 Saga:

The Tech: