hono-preact
Overview
Quick Start
The Route Table
Layouts & Nesting
Adding Pages
Active Links
Server Loaders
Loading States
Reloading Data
Prefetching
Streaming
Live Loaders
Realtime Channels
Server Actions
Validation
Optimistic UI
View Transitions
Middleware
CSRF Protection
CLI
Vite Config
Project Structure
Composing Hono Middleware
WebSockets
Rooms & Presence
renderPage
Link Prefetch
Build & Deploy
Overview
Dialog
Popover
Tooltip
Menu
Context Menu
Select
Combobox
Toast
renderElement
useControllableState
mergeRefs
useListNavigation
useTypeahead
useListboxSelection
usePosition
usePositioner
useDismiss
useFocusReturn
useSafeArea
usePresence

View Transitions#

The framework wraps every same-document route change in document.startViewTransition automatically. You don't opt in. Style the default root transition with ::view-transition-old(root) and ::view-transition-new(root), and respect prefers-reduced-motion.

On top of that, three primitives let you scale view transitions across many elements, hook into the navigation lifecycle, and target CSS by direction.

Named elements#

Use <ViewTransitionName> to give an element a stable identity that participates in the transition. The component is polymorphic (Base UI renderElement style): pass a render prop to control which element actually mounts.

import { ViewTransitionName } from 'hono-preact';

// list page
{
  posts.map((post) => (
    <ViewTransitionName
      key={post.id}
      name={`post-${post.id}`}
      groupClass="post-card"
      render={<article class="card" />}
    >
      <h2>{post.title}</h2>
    </ViewTransitionName>
  ));
}

// detail page
<ViewTransitionName name={`post-${post.id}`} render={<header />}>
  <h1>{post.title}</h1>
</ViewTransitionName>;

The matching name between list and detail tells the browser to animate the elements as a continuous group.

Props#

PropTypeDefaultDescription
namestring | null | undefinedrequiredThe view-transition-name to apply; null/undefined clears it.
groupClassstring | string[]noneAdds a view-transition-class for grouping.
renderVNode | string | ((props) => VNode)<div>The element to render: a tag string, an element, or a function.
childrenComponentChildrennoneContent.

For hand-written components, the useViewTransitionName hook returns a ref callback:

import { useViewTransitionName } from 'hono-preact';

function PostCard({ post }: { post: Post }) {
  const vt = useViewTransitionName(`post-${post.id}`);
  return <article ref={vt}>{post.title}</article>;
}

<ViewTransitionGroup class="post-card"> (or useViewTransitionClass) sets view-transition-class so you can target many elements via ::view-transition-group(.post-card) in CSS.

Lifecycle hooks#

useViewTransitionLifecycle exposes four phases the framework controls:

import { useViewTransitionLifecycle } from 'hono-preact';

useViewTransitionLifecycle({
  onBeforeTransition: (event) => {
    // Before the View Transition starts.
    // event.types.push('my-type') to add a type, event.skip() to bypass.
  },
  onBeforeSwap: (event) => {
    // After the framework has begun the transition. Last chance to mutate
    // the DOM before the new-frame snapshot is captured.
  },
  onAfterSwap: (event) => {
    // The new DOM is settled and the browser is ready to animate.
  },
  onAfterTransition: (event) => {
    // After transition.finished resolves (or rejects).
    // event.reason is 'skipped' | 'unsupported' | 'aborted' if the transition
    // didn't run.
  },
});

Each callback receives a ViewTransitionEvent:

MemberTypeDescription
tostringDestination path.
fromstring | undefinedSource path; undefined on initial load.
direction'initial' | 'push' | 'replace' | 'back' | 'forward'Navigation direction.
typesstring[]Mutable list of transition type names.
skip()() => voidSkip this transition.
set(key, value) / get(key)methodPer-event scratch shared across callbacks.

The four callbacks (onBeforeTransition, onBeforeSwap, onAfterSwap, onAfterTransition) each have the signature (event) => void | Promise<void>.

Direction-driven CSS via types#

The framework adds three types to every transition:

  • nav-initial on the first navigation after hydrate, otherwise one of nav-push, nav-replace, nav-back, nav-forward.
  • nav-same-origin.

Target them with :active-view-transition-type(...):

:active-view-transition-type(nav-back) ::view-transition-old(root) {
  animation: slide-right-out 0.3s ease;
}
:active-view-transition-type(nav-back) ::view-transition-new(root) {
  animation: slide-right-in 0.3s ease;
}

Add your own types with useViewTransitionTypes:

import { useViewTransitionTypes } from 'hono-preact';

useViewTransitionTypes((nav) =>
  nav.from?.startsWith('/posts/') && nav.to === '/posts' ? ['back-to-list'] : []
);

For a rule that should apply regardless of what is mounted, for example a calm transition whenever a navigation enters or leaves a whole section, use the always-on subscribeViewTransitionTypes. A hook in a section layout only sees navigations within the section: it is not subscribed yet when you navigate in, and is torn down before you navigate out. A single subscriber registered at client startup sees every navigation's from and to.

import { subscribeViewTransitionTypes } from 'hono-preact';

subscribeViewTransitionTypes((nav) => {
  const inDocs = (p?: string) => p === '/docs' || p?.startsWith('/docs/');
  return inDocs(nav.to) || inDocs(nav.from) ? ['docs'] : [];
});

It returns an unsubscribe and is a no-op on the server, so it is safe to register as a module side effect.

See also#

  • Optimistic UI: the transition option on useOptimistic and useOptimisticAction wraps mutations in a view transition.
  • Loading States: coordinating loading indicators with transitions.