GioJSdocs
On this page

<GioLink>

A link that navigates on the client, prefetches the next page ahead of the click, and can animate the swap with a view transition.

app/(site)/layout.tsx
import type { LayoutProps } from '@gio.js/core';
import { GioLink } from '@gio.js/react';

export default function SiteLayout({ children }: LayoutProps) {
  return (
    <>
      <nav>
        <GioLink href="/">Home</GioLink>
        <GioLink href="/blog">Blog</GioLink>
      </nav>
      {children}
    </>
  );
}

GioLink renders a real <a href>, so the link works before the page hydrates and in a browser without JavaScript. Once hydrated, a plain click fetches the next page and renders it into the same React root: shared layouts keep their state, and nothing is reloaded.

Reference

PropTypeDefaultDescription
href (required)string-A path starting with / (written out or built with href()), or a same-page #hash. Anything else is a plain link the browser follows.
prefetch'hover' | 'viewport' | false'hover'When to fetch the page ahead of the click. See prefetch.
transition'fade' | 'slide-left' | 'slide-up' | 'scale' | falsefalseAnimate the swap with a view transition. See transition.
replacebooleanfalseReplace the current history entry instead of adding one.
scrollbooleantrueScroll to the top of the new page, or to the element its #hash names. false keeps the scroll position.
children (required)React.ReactNode-The link content.
classNamestring-Passed to the <a>.
targetReact.HTMLAttributeAnchorTarget-Passed to the <a>. Any value other than _self leaves the click to the browser.
downloadstring | boolean-Passed to the <a>. A download link is always left to the browser.
aria-current'page' | 'step' | 'location' | 'date' | 'time' | boolean-Passed to the <a>. Mark the link to the current page with "page".

These are all the props GioLink takes: id, style, rel, title, onClick and data-* attributes are not forwarded, and TypeScript rejects them. Wrap the link, or use a plain <a> where you need them.

href

A click is handled by the client router only when href starts with / (but not //) or with #. Every other value - https://..., mailto:, a protocol-relative //cdn.example.com, a relative about or a query-only ?page=2 - is left to the browser: a normal full page load, with no prefetch. Write the full path (/blog?page=2) to keep a query change on the client.

prefetch

  • 'hover' (default) - fetches the page when the pointer enters the link, a moment before the click. Links the visitor never points at cost nothing.
  • 'viewport' - fetches the page once, when the link first scrolls into view (an IntersectionObserver). Use it for the few links a visitor is likely to follow next; a long list of them sends one request per link.
  • false - never prefetches. The click still navigates on the client.

A prefetch is a GET with Purpose: prefetch and Sec-Purpose: prefetch. It is skipped for a #hash link, for the page already on screen, and for a page already in the prefetch cache. The cache keeps a page for 30 seconds (PREFETCH_TTL_MS), holds at most 50, and is emptied by router.refresh(), by every <GioForm> submission and by any fetch() to your own origin other than GET, HEAD or OPTIONS. The server budgets prefetches per client (429 once the budget is spent, or for every prefetch with [prefetch] enabled = false); a prefetch that failed never decides the click - the navigation fetches the page itself. See [prefetch].

transition

With a preset, the DOM swap runs inside document.startViewTransition(), and data-gio-transition="<preset>" is set on <html> until the transition finishes. The keyframes come in a <style> element that React hoists into the head once per page. The preset type is exported as TransitionPreset.

PresetOld pageNew pageDuration
fadefades outfades in200 ms
slide-leftmoves 40px left, fadingcomes in from 40px right220 ms
slide-upmoves 24px up, fadingcomes in from 24px below220 ms
scalegrows to 104%, fadinggrows from 96%200 ms

Browsers without the View Transitions API swap the page without an animation, and prefers-reduced-motion: reduce turns the animation off. Back and forward never animate. To restyle a preset, target :root[data-gio-transition="fade"]::view-transition-new(root) in your own CSS.

Behavior

The browser handles the click itself - a normal navigation - when:

  • a modifier key is held (Ctrl, Cmd, Shift or Alt) or a button other than the left one is used, so "open in new tab" works;
  • target is set to anything but _self, or download is set;
  • href is not a / path or a #hash (see href).

Otherwise the router takes over: it uses a fresh prefetch or fetches the page, loads the route's client chunk and new stylesheets, renders the page in place, updates history, scrolls, and moves focus to the new <main>. A link to the URL already shown replaces the history entry, as browsers do. A #hash link to the current page only scrolls. The navigation becomes a full page load when the answer is not a GioJS page (JSON, a 503, a static host's 404.html), when the network fails, and when a new deployment went live since the page loaded. The details are in How a soft navigation works.

Examples

Linking to a dynamic route

Build the path with href(): the pattern autocompletes from your app/ directory, and a missing or misspelled param is a type error.

app/blog/page.tsx
import { GioLink, href } from '@gio.js/react';

interface Post {
  slug: string;
  title: string;
}

export default function Blog({ posts }: { posts: Post[] }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.slug}>
          <GioLink href={href('/blog/:slug', { slug: post.slug })}>{post.title}</GioLink>
        </li>
      ))}
    </ul>
  );
}

Compare the link with usePathname() and set aria-current, which screen readers announce and CSS can target (a[aria-current="page"]). Put the navigation in a layout below the root layout, such as a route group's, so it re-renders on every soft navigation.

app/(site)/nav.tsx
import { GioLink, usePathname } from '@gio.js/react';

const LINKS = [
  { href: '/', label: 'Home' },
  { href: '/blog', label: 'Blog' },
  { href: '/about', label: 'About' },
];

export function Nav() {
  const pathname = usePathname();
  return (
    <nav>
      {LINKS.map((link) => {
        const active =
          pathname === link.href || (link.href !== '/' && pathname.startsWith(`${link.href}/`));
        return (
          <GioLink key={link.href} href={link.href} aria-current={active ? 'page' : undefined}>
            {link.label}
          </GioLink>
        );
      })}
    </nav>
  );
}

Tabs that keep the scroll position

Tabs that only change the query should neither scroll to the top nor add a history entry per click. Write the full path - a query-only href is a full page load.

app/products/[id]/tabs.tsx
import { GioLink, usePathname, useSearchParams } from '@gio.js/react';

const TABS = ['details', 'reviews', 'shipping'];

export function Tabs() {
  const pathname = usePathname();
  const current = useSearchParams().get('tab') ?? 'details';
  return (
    <div role="tablist">
      {TABS.map((tab) => (
        <GioLink
          key={tab}
          href={`${pathname}?tab=${tab}`}
          replace
          scroll={false}
          aria-current={tab === current ? 'page' : undefined}
        >
          {tab}
        </GioLink>
      ))}
    </div>
  );
}

Prefetching the next step

A call-to-action the visitor will probably click can be fetched as soon as it is on screen. Links that are rarely followed can opt out.

tsx
<GioLink href="/checkout" prefetch="viewport">Continue to checkout</GioLink>
<GioLink href="/terms" prefetch={false}>Terms of service</GioLink>

Animating page changes

tsx
<GioLink href="/gallery/2" transition="slide-left">Next photo</GioLink>
<GioLink href="/gallery" transition="fade">Back to the gallery</GioLink>

GioLink works for these, but it adds nothing: they are left to the browser. A plain <a> also lets you set rel.

tsx
<a href="https://github.com/Ggaming5005/GioJS" target="_blank" rel="noreferrer">GitHub</a>
<GioLink href="/reports/2026.pdf" download>Download the report</GioLink>

Good to know

  • In the root layout a GioLink is a plain link. app/layout.tsx is server-rendered HTML that never hydrates, so a link there neither prefetches nor navigates on the client. Put site navigation in a route group's layout (app/(site)/layout.tsx), as the create-giojs starter does.
  • Keyboard focus does not prefetch; only the pointer entering the link (or, with 'viewport', the link becoming visible) does. Where IntersectionObserver is missing, 'viewport' never prefetches.
  • After a mutation (a <GioForm> post or any same-origin POST/PUT/PATCH/DELETE fetch()), pages prefetched before it are dropped, so a click never shows data from before the change.
  • In a static export, links navigate on the client on any static host; the server's prefetch budget does not exist there.
  • Shift-click and middle-click open the link the browser's way. This cannot be turned off.
  • router.push() and router.replace() take the same transition option. The preset keyframes arrive with the first GioLink on the page that has a transition; without one, a view transition started from code runs the browser's default cross-fade.

Version history

VersionChanges
v0.1.0-beta.8Added replace and scroll. Prefetched pages expire after 30 seconds and are dropped after a mutation; a failed prefetch no longer turns the click into a full page load; a click after a new deployment loads the new build in full.
v0.1.0-beta.6prefetch="viewport" implemented.
v0.1.0-beta.5Modified clicks, target="_blank" and download links are left to the browser; a failed client navigation falls back to a full page load.
v0.1.0-beta.1Introduced.