Don't cross-fade out-of-view elements in your View Transition
duyda · May 9, 2026 · 2min
On this page
So, what is the problem...?
An out-of-view element is simply an element outside the current viewport. With the View Transition API, these elements can still get crossfaded during transitions.
That results in unexpected motion where off-screen elements move around or fade across the page. It looks glitchy and feels distracting, especially on long scrolling pages.
Therefore, I think only visible elements should participate in the transition.
Viewport-aware View Transition
The name sounds fancy, but the idea is actually super simple: temporarily disable view-transition-name once an element goes out of view. Thankfully, Intersection Observer makes it really easy.
Here's a quick and naive example in Svelte using an attachment:
<script lang="ts">
function viewTransition(name: string): Attachment<HTMLElement> {
const observer = new IntersectionObserver(([entry]) => {
node.style.viewTransitionName = entry?.isIntersecting ? name : 'none';
});
return (node) => {
observer.observe(node);
return () => observer.disconnect();
};
}
</script>
<div {@attach viewTransition('title')}>...</div>
Honestly, this works decently, although it lacks proper clean-up.
And once you start doing this for multiple elements across different pages, things get repetitive real fast. Every element creates its own observer, clean-up logic gets duplicated, and the whole thing becomes harder to maintain.
A better approach is creating a utility that:
- reuses
IntersectionObserveras much as possible for elements sharing the same options - handles cleanup automatically
- disconnects itself once nothing is being observed anymore
And I built one like that to make viewport-aware transitions feel effortless.
Welcome to the leaveView utility
I first noticed this issue while building this very long-scrolling blog.
During view transitions, elements were cross-fading from outside the viewport, and honestly... it looked pretty weird. Not exactly the kind of motion I expect and want my readers noticing.
So I ended up building a small utility for it, now available in @duydang2311/sveltecraft since v0.0.6.
The utility we'll care about, called leaveView, is a small general-purpose attachment that runs whenever an element leaves the viewport, then optionally returns a clean-up function that runs once the element comes back into view again.
<script lang="ts">
import { leaveView } from '@duydang2311/sveltecraft';
</script>
<!-- overload 1: leaveView(attachment) -->
<div
style:view-transition-name="post-title"
{@attach leaveView((node: HTMLDivElement) => {
node.style.viewTransitionName = 'none';
return () => {
node.style.viewTransitionName = 'post-title';
};
})}
>
...
</div>
<!-- overload 2: leaveView(node, attachment) -->
<!-- better typescript type inferrence -->
<div
style:view-transition-name="post-title"
{@attach (node) =>
leaveView(node, (node) => {
node.style.viewTransitionName = 'none';
return () => {
node.style.viewTransitionName = 'post-title';
};
})}
>
...
</div>
And because of the utility internal logic, you also get a few nice bonuses for free:
- one shared observer for your elements
- clean-up handling
- observer disconnection once nothing is being observed anymore
That said, leaveView is still intentionally general-purpose. So for this specific View Transition use case, how about we trying to make the API even nicer? Something like:
// dom.ts
export function viewTransition<T extends HTMLElement>(name: string): Attachment<T> {
return (node) => {
// this won't be able to run during SSR or non-JS browser
// but guess what
// it's fine because View Transition API requires JS anyway
node.style.viewTransitionName = name;
return leaveView(node, (node: T) => {
node.style.viewTransitionName = 'none';
return () => {
node.style.viewTransitionName = name;
};
});
};
}
<script lang="ts">
import { viewTransition } from './dom';
</script>
<div {@attach viewTransition('post-title')}>...</div>
Bang! Just like that, your transition became much more consistent.
If you enjoy this, why not try giving my other Svelte utilities a try too?
All available on @duydang2311/sveltecraft.