
Devanzire
portfolio and visual system
Overview
The site you are looking at. A hand-built design system on Next.js, with continuous route transitions and a WebGL sphere that persists across pages.
The problem
A portfolio that reloads and cross-fades on every navigation feels like a document, not a product. Between one page and the next there is a white flash, and that flash is the admission that nothing was ever continuous: two documents, served one after the other.
I wanted the opposite. Pages that become one another, and something that survives the change to prove it.
The sphere survives the route
The background sphere lives in the layout, not in the page. That is the whole trick: because the layout is never unmounted on navigation, the sphere is never rebuilt. Each route declares where it wants the sphere to sit — lateral shift, scale, two rotations — and the sphere travels to that pose.
The poses are not arbitrary. articles and projects share exactly the same
one, the home's open pose, so entering either leaves the sphere untouched and
lets the panel morph read on its own. Both long-form routes push it further out
and shrink it, because there its job is to stay clear of the text column.
The home declares no pose at all, and that absence is deliberate: while the home is mounted the reveal hook owns the sphere's body outright, and a second system tweening the same values would fight it.
/media/projects/devanzire-portfolio/01-poses.webpThe panel hands over its box
The home has a panel that opens. Navigating from it, that panel measures where it is on screen and hands the rectangle to the next page, whose frame grows out of it rather than appearing. It is a FLIP, but the interesting part is where the measurement lives.
Not in React context: the capture happens inside the click handler and is read after a route change, so the producer and the consumer are never mounted at the same time. There is no shared subtree to put it in. It lives at module scope, which is exactly the scope of the problem.
And it expires. A capture is good for the navigation it was made for; without a lifetime, a click that never navigated would leave a rectangle behind and the next unrelated page load would grow out of a box nobody ever saw.
/media/projects/devanzire-portfolio/02-morph.webpOne writer per property
This was the hard part, and it was not a bug so much as a category of bug.
The first version had React rendering geometry from ternaries while GSAP wrote those same properties. Both were correct in isolation. Together, the result depended on which ran last — which is another way of saying there was no result, there was a race.
The rule that came out of it is that React decides what state a thing is in, via
data-state, and CSS or GSAP decide how it moves. Never both writing left,
top or width. Everything else follows from it:
- The initial state goes in CSS, not
gsap.set, so there is no flash before hydration. - Only
transformandopacityon anything that animates per frame. - Interactivity is never gated on an animation finishing. Listeners mount immediately; the animation is only the visual part.
- Nothing invisible stays operable. Opacity 0 leaves an element clickable and
reachable by keyboard, so it travels with
inert.
What got written down
The motion layer has a documented behaviour matrix — what the loader, the page entrance and the home deck each do in every scenario — and it is verified in development and production separately.
The reason is Strict Mode: it only runs in next dev, and it mounts, cleans up
and remounts every effect on the first render. Animation effects are not
idempotent, so the two environments genuinely differ. A failure that only shows
up in development is still a real failure. Strict Mode does not cause it, it
reveals it.
Live result
Devanzire


