Site Motion Log 01: Motion Map, First Visit, and One H
This pass split site motion into clear layers, then rebuilt the first-visit intro so it feels intentional without slowing the site down.
Adding motion to a website is easy. Adding the right amount is harder.
The rule for this pass was simple: motion can help content arrive, but it should not make visitors wait. The first visit can have a small ritual. Everything else should be short and useful.
The Motion Map
The site now has a few clear motion layers:
| File | Role | Scope |
|---|---|---|
HomeEntrance.tsx | First-visit intro and home ready state | Only before the homepage first screen |
Header.tsx | Header entrance and active nav highlight | Global header |
Hero.tsx | Welcome text, calligraphy image, name card, scroll cue | Homepage first screen |
HomeReveal.tsx | Scroll reveal after the intro is done | Homepage sections below the hero |
PageTransition.tsx | Route boundary | Structural only, no visual transition |
RouteLoading.tsx | Loading fallback | Screen-reader status only |
PageEnter.tsx | Local page entry | Blog, gallery, calligraphy, about |
PhotoModal.tsx / CalligraphyModal.tsx | Modal enter and exit | Gallery and calligraphy details |
TableOfContents.tsx | Active item movement | Blog detail page |
BackToTop.tsx / ScrollChrome.tsx | Back-to-top and scroll feedback | Long pages |
globals.css | Typewriter text, marquee text, reduced motion rules | CSS layer |
This split keeps the boundaries clear. The homepage intro does not own secondary pages. Route loading does not replay the homepage entrance.
The Intro Plays Once
The first-visit intro in HomeEntrance.tsx does three things:
- Shows a blackletter
H. - Shows the current date and time with
We Are the World. - Releases the homepage and header after the overlay leaves.
The time label is formatted in the browser:
return `${parts.year}.${parts.month}.${parts.day} ${parts.hour}:${parts.minute}`;The welcome line uses CSS for the type effect:
.home-intro-type {
clip-path: inset(0 100% 0 0);
animation: home-intro-type 0.82s steps(35, end) 0.12s both;
}The text itself renders once. CSS reveals it from left to right. That avoids the earlier flicker where the small text appeared, disappeared, then appeared again.
State Matters More Than Timers
The early problem was order. The homepage, header, and overlay were all moving on separate timelines.
Now the intro has explicit phases:
type IntroPhase = "checking" | "intro" | "exiting" | "done";checking decides whether the intro should be skipped. intro shows it. exiting fades the overlay out. Only after that does the page become ready.
That ready state is shared through context. Components do not guess when to appear. They wait for the same signal.
Why There Are Two Records
The intro uses sessionStorage:
const INTRO_STORAGE_KEY = "huzejun.homeIntroPlayed";That handles refreshes. But returning to the homepage from another route can still remount components, so there is also a runtime flag:
let introCompletedInRuntime = false;sessionStorage remembers across reloads. The runtime flag protects the current app session from a quick overlay flash.
Takeaway
The intro is not a global loading screen. It is a short curtain at the entrance.
Once that boundary is clear, the rest of the motion becomes easier: the intro exits, the first screen enters, scroll sections reveal when seen, and secondary pages handle their own local entrance.

