<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.
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
| Prop | Type | Default | Description |
|---|---|---|---|
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' | false | false | Animate the swap with a view transition. See transition. |
replace | boolean | false | Replace the current history entry instead of adding one. |
scroll | boolean | true | Scroll 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. |
className | string | - | Passed to the <a>. |
target | React.HTMLAttributeAnchorTarget | - | Passed to the <a>. Any value other than _self leaves the click to the browser. |
download | string | 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 (anIntersectionObserver). 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.
| Preset | Old page | New page | Duration |
|---|---|---|---|
fade | fades out | fades in | 200 ms |
slide-left | moves 40px left, fading | comes in from 40px right | 220 ms |
slide-up | moves 24px up, fading | comes in from 24px below | 220 ms |
scale | grows to 104%, fading | grows 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;
targetis set to anything but_self, ordownloadis set;hrefis not a/path or a#hash(seehref).
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.
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>
);
}Marking the active link
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.
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.
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.
<GioLink href="/checkout" prefetch="viewport">Continue to checkout</GioLink>
<GioLink href="/terms" prefetch={false}>Terms of service</GioLink>Animating page changes
<GioLink href="/gallery/2" transition="slide-left">Next photo</GioLink>
<GioLink href="/gallery" transition="fade">Back to the gallery</GioLink>Links to other sites and files
GioLink works for these, but it adds nothing: they are left to the browser. A plain <a> also lets you set rel.
<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
GioLinkis a plain link.app/layout.tsxis 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 thecreate-giojsstarter does. - Keyboard focus does not prefetch; only the pointer entering the link (or, with
'viewport', the link becoming visible) does. WhereIntersectionObserveris missing,'viewport'never prefetches. - After a mutation (a
<GioForm>post or any same-originPOST/PUT/PATCH/DELETEfetch()), 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()androuter.replace()take the sametransitionoption. The preset keyframes arrive with the firstGioLinkon the page that has atransition; without one, a view transition started from code runs the browser's default cross-fade.
Related
- Linking & Navigating - how soft navigation, scroll and focus work.
useRouter- navigate from code.usePathname- mark the active link.href- typed paths for dynamic routes.navigate- navigate outside components.<LocaleLink>- aGioLinkthat adds the locale prefix.[prefetch]- the server's prefetch budget.
Version history
| Version | Changes |
|---|---|
v0.1.0-beta.8 | Added 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.6 | prefetch="viewport" implemented. |
v0.1.0-beta.5 | Modified 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.1 | Introduced. |