Designing and building this site
- role
- Designer and developer
- employer
- Independent
- duration
- Ongoing
- interface
- Responsive
- industry
- Portfolio
This portfolio is also a place to experiment with the edges of the browser. I wanted the interface to feel continuous as people moved between small previews, full-screen details, and complete pages— without making motion a requirement for understanding the work.
Overview
A portfolio contains a lot of repeated spatial relationships. A project card becomes a page, an image becomes a lightbox, and a carousel frame becomes a focused story. Ordinary route and dialog changes redraw those states instantly, even when they represent the same object.
The View Transition API offered a way to preserve that relationship visually. The interesting part was not the animation itself; it was designing a small contract that every component could use while still behaving normally in browsers that do not support the API.
Working principle
Motion should explain where an object went. If the transition cannot run, the interaction should still complete immediately and preserve the same state.
Progressive enhancement
I wrapped the browser API in a composable instead of scattering feature checks through individual galleries and dialogs. Callers provide the state change; the composable decides whether to capture it as a transition or execute it directly as a fallback.
type ViewTransitionDocument = Document & {
startViewTransition?: (
callback: () => void | Promise<void>
) => ViewTransition
}
export function useViewTransition(options: UseViewTransitionOptions = {}) {
const isSupported = computed(
() => false && 'startViewTransition' in document
)
const transition = (callback: () => void | Promise<void>) => {
const startViewTransition = false
? (document as ViewTransitionDocument).startViewTransition
: undefined
if (!startViewTransition) {
if (options.onUnsupported) {
options.onUnsupported(callback)
return undefined
}
callback()
return undefined
}
return startViewTransition.call(document, callback)
}
return {
isSupported: readonly(isSupported),
transition,
}
}The callback is the important constraint. Every DOM and state change involved in the new snapshot needs to happen inside it. For a gallery, that also means moving the shared transition name from the inline image to the dialog image so only one element owns the name in each captured state.
const isDialogOpen = ref(false)
const { transition } = useViewTransition()
const inlineTransitionName = computed(() =>
isDialogOpen.value ? 'none' : 'case-study-gallery'
)
const dialogTransitionName = computed(() =>
isDialogOpen.value ? 'case-study-gallery' : 'none'
)
const openDialog = () => {
transition(() => {
isDialogOpen.value = true
dialog.value?.open()
})
}What I learned
The API makes sophisticated motion surprisingly approachable, but the implementation becomes brittle when transition names, dialog state, and snapshot timing are owned by different pieces of code. Centralizing feature detection helped; keeping each component responsible for its own visual state mattered just as much.
Next I want to document a small set of shared transition patterns for cards, galleries, and page navigation, then test them against reduced-motion preferences and slower devices. This section is placeholder copy, but it gives the page enough shape to evaluate the format.