PageFade

The content area of a page, cross-faded when the route changes.

Overview

A site where the frame stays put and only the content between pages should fade.

Changing area

Do
The overview page.
import { useState } from "react";
import {
  Body,
  PageFade,
  SegmentedControl,
  Stack,
  startPageTransition,
} from "@hackcove/ui";

function Example() {
  const [value, setValue] = useState("overview");
  const items = [
    { value: "overview", label: "Overview" },
    { value: "activity", label: "Activity" },
  ];
  return (
    <Stack gap="sm">
      <SegmentedControl
        label="Page"
        items={items}
        value={value}
        onChange={(next) => startPageTransition(() => setValue(next))}
      />
      <PageFade>
        <Body>The {value} page.</Body>
      </PageFade>
    </Stack>
  );
}
PageFade wraps only the content that changes between pages; the frame around it, with the control, stays put.
Don't
The overview page.
import { useState } from "react";
import {
  Body,
  PageFade,
  SegmentedControl,
  Stack,
  startPageTransition,
} from "@hackcove/ui";

function Example() {
  const [value, setValue] = useState("overview");
  const items = [
    { value: "overview", label: "Overview" },
    { value: "activity", label: "Activity" },
  ];
  return (
    <PageFade>
      <Stack gap="sm">
        <SegmentedControl
          label="Page"
          items={items}
          value={value}
          onChange={(next) => startPageTransition(() => setValue(next))}
        />
        <Body>The {value} page.</Body>
      </Stack>
    </PageFade>
  );
}
Wrapping the frame too makes the control fade with the content, so the page flickers instead of changing in place.

Transition

Do
The overview page.
import { useState } from "react";
import { Body, Button, PageFade, Stack, startPageTransition } from "@hackcove/ui";

function Example() {
  const [value, setValue] = useState("overview");
  return (
    <Stack gap="sm">
      <Button
        variant="outline"
        onClick={() =>
          startPageTransition(() => setValue(value === "overview" ? "activity" : "overview"))
        }
      >
        Switch page
      </Button>
      <PageFade>
        <Body>The {value} page.</Body>
      </PageFade>
    </Stack>
  );
}
The change goes through startPageTransition, so the browser has the old and the new page to fade between.
Don't
The overview page.
import { useState } from "react";
import { Body, Button, PageFade, Stack } from "@hackcove/ui";

function Example() {
  const [value, setValue] = useState("overview");
  return (
    <Stack gap="sm">
      <Button
        variant="outline"
        onClick={() => setValue(value === "overview" ? "activity" : "overview")}
      >
        Switch page
      </Button>
      <PageFade>
        <Body>The {value} page.</Body>
      </PageFade>
    </Stack>
  );
}
A plain state change swaps the content at once: there is no transition to fade.

Usage

  • Wrap what changes between pages, once per page, and leave the header and footer outside it.
  • Start the change inside a view transition: startPageTransition(() => setPage(next)) for a state change, or the router's own option (TanStack Router viewTransition, React Router's viewTransition, Astro's ClientRouter). Without one nothing fades.
  • Two PageFades on one page each need their own name.
  • Portal an overlay such as a dropdown to the body: the fading area is a stacking context, so an overlay inside it cannot rise above a sticky header outside it.

Accessibility

  • A PageFade is a plain <div> with no role, so it changes nothing for assistive technology.
  • Under prefers-reduced-motion: reduce the content swaps with no fade, and startPageTransition just runs the update.
  • Where the browser has no View Transitions it is a plain <div> and the content swaps.

API

PageFade

The page's content area, named for the View Transitions API so a route change cross-fades it while the frame around it (header, footer) stays put. Wrap what changes between pages, once per page: two on one page need a different name each, or the browser skips the transition. The fade runs only when the navigation is wrapped in a view transition: startPageTransition, or a router's own option (TanStack Router viewTransition, React Router's viewTransition prop, Astro's ClientRouter). Otherwise, or where the browser has no View Transitions, it renders and behaves as a plain <div>. A view-transition-name makes the div a stacking context, so a dropdown inside it cannot rise above a sticky header outside it: portal overlays to the body. Under prefers-reduced-motion: reduce this content swaps with no fade; a router's own transition of the rest of the page is the router's to quiet.


name

The view-transition name, a CSS identifier (letters, digits, -, _).

Type
string
Default
hc-page

Functions

  • startPageTransition runs a synchronous state change inside a view transition, so a PageFade cross-fades; call it from an event handler.