Best Practices7 min read
Animation should explain, confirm or delight, never make people wait. The rules I use to decide what moves, how fast it moves and what it costs.

My portfolio has a lot of motion: a solar system in the hero, a pinned project coverflow and a MacBook that turns as you reach the contact form. Building it made me write down the rules I had been applying by instinct. Motion is the easiest way to make an interface feel expensive, and just as easy a way to make it feel slow, so every animation has to earn its place.
The first question is frequency. How often will someone see this animation?
The second question is purpose. Good reasons are feedback, a visible change of state, spatial continuity and explaining how something works. If the only reason is that it looks cool and people will see it often, it goes.
Interface motion stays under about 300ms. Things that enter use a strong ease-out, so they react immediately and settle gently. Exits are faster than entrances. I never use ease-in for UI: it delays the start of the movement, which is exactly the moment people are watching.
:root {
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
}
.button {
transition: transform 160ms var(--ease-out);
}
.button:active {
transform: scale(0.97);
}That tiny press state is my favourite detail on any site. It costs one line and makes every button feel like it heard you.
Transform and opacity skip layout and paint, so the browser can animate them off the main thread. Width, height, top, left and margins force layout on every frame. When a page is busy hydrating or loading data, that difference decides whether an animation is smooth or visibly stutters. I also keep blur small and short, because large blurs are expensive, especially in Safari.
The 3D MacBook in my contact section is a 10 MB model, so it follows four rules:
<CanvasBoundary>
<Canvas
dpr={[1, 1.75]}
frameloop={inView ? "always" : "never"}
>
<MacBook progress={scrollProgress} />
</Canvas>
</CanvasBoundary>prefers-reduced-motion is not a request for a broken page. I remove movement, parallax and anything that spins, and keep opacity and colour changes that communicate state. Pinned galleries become a simple swipeable row. The content and the story stay exactly the same.
@media (prefers-reduced-motion: reduce) {
.orbit,
.parallax {
animation: none;
}
}Two metrics catch most motion mistakes. LCP suffers when the largest element starts invisible and fades in after a long delay, so entrances start immediately and never hide the main content for long. INP suffers when animation work competes with input handling on the main thread. I check both in the Performance panel, and I review every important animation in slow motion the next day, when I am no longer attached to it.
Motion done this way is almost invisible as a technique. People do not remember the easing curve. They remember that the site felt fast, calm and a little bit special.

Web Development8 min read
How I rebuilt my portfolio's scroll choreography with animation-timeline, view() and scroll(): no scroll listeners, smoother frames and graceful fallbacks.

Technical7 min read
The hero of my portfolio is a CSS-only solar system: tilted rings, planets that stay upright and real depth sorting. Here is the transform chain behind it.

Web Development5 min read
Explore the powerful features introduced in React 19.2, including the Compiler, Activity Component, and useEffectEvent, which enhance performance and code quality.