Disable named view transition on admin page-content wrapper
Deploy to route.crispygoat.com / deploy (push) Successful in 4m12s

The <ViewTransition name="page-content"> wrapper around the admin
main content was triggering a 'shared element' morph on every page
load and navigation: the browser tries to animate the bounding box
of the named element across the old and new pages, and since each
admin page has a different height, the browser was scaling the
snapshot from the top-left corner. That read as the page 'loading
from the corner with keyframes' and pushed the bottom of the
content to the top during the transition.

Setting update="none" opts out of the named transition entirely.
The children just swap on navigation — no fade, no scale, no morph.
The AdminSidebar + parchment background stay mounted across
navigations so the cut reads as a content swap inside a stable
shell, which is what the eye expects for a logged-in admin app.

The ::view-transition-old(page-content) / ::view-transition-new
CSS rules in globals.css are left in place in case we want to
re-enable with a different shape later (pure-opacity rules on the
root pseudo-elements instead of a named wrapper).
This commit is contained in:
Tyler
2026-06-17 10:40:46 -06:00
parent 0a534222e8
commit 4b781d3c76
@@ -4,48 +4,29 @@ import { ViewTransition } from "react";
import type { ReactNode } from "react";
/**
* Smooth view-transition wrapper for any page-section content.
* Page-content wrapper. The named `page-content` view transition used
* to apply a CSS-driven crossfade (::view-transition-old/new rules in
* globals.css) on every navigation. The opacity fade itself was fine,
* but because the named transition attaches `view-transition-name:
* page-content` to the wrapper, the browser also tries to morph the
* wrapper's bounding box across pages — and since each admin page has
* a different height, the browser was scaling the snapshot from the
* top-left corner, which read as the page "loading from the corner"
* and pushed the bottom of the content to the top during the
* transition.
*
* Drop this around the main content area of a route (or around an
* individual <Suspense> chunk) to opt that subtree into a soft
* crossfade on navigation. The browser's native View Transitions API
* handles the actual animation — when the browser doesn't support it,
* the children just swap, no error.
* `update="none"` opts out of the named transition entirely. The
* children just swap on navigation (no fade, no scale, no morph) —
* the AdminSidebar + parchment background stay mounted across
* navigations so the cut reads as a content swap inside a stable
* shell, which is what the eye expects for a logged-in admin app.
*
* `name` groups elements so the browser can morph the same element
* across pages. Use the same name on the source and destination (e.g.
* the page header) for shared-element morphing. Use the default
* `"page-content"` for plain crossfades.
*
* Motion note: the CSS-side ::view-transition-old/new rules are a
* 90/140ms pure-opacity fade with NO translate. Earlier versions
* shifted the page 4-6px on enter/exit which compounded with the route
* content settling into place and read as a small "drop" — bad for
* vestibular comfort. A flat opacity crossfade softens the cut
* without any positional movement.
*
* Usage:
* <main>
* <SmoothViewTransition>{children}</SmoothViewTransition>
* </main>
* If we ever want a subtle crossfade back, the right shape is a
* pure-opacity ::view-transition-old/new pair with NO named wrapper
* (i.e. drop the `name` prop and add the rules to
* ::view-transition-old(root) / ::view-transition-new(root) so the
* browser doesn't morph the bounding box).
*/
export function SmoothViewTransition({
children,
name = "page-content",
}: {
children: ReactNode;
name?: string;
}) {
return (
<ViewTransition
name={name}
// `default` runs a CSS-driven crossfade using the
// ::view-transition-* pseudo-elements defined in globals.css.
// Other options: "none" (instant cut), or a custom string your
// CSS can match.
update="default"
>
{children}
</ViewTransition>
);
export function SmoothViewTransition({ children }: { children: ReactNode }) {
return <ViewTransition update="none">{children}</ViewTransition>;
}