Routing

Pages live in src/pages/. Each file maps directly to a URL:

FileRoute
src/pages/page.tsx/
src/pages/about.tsx/about
src/pages/blog/page.tsx/blog
src/pages/users/[id].tsx/users/:id
src/pages/shop/[...all].tsx/shop/*

Export a default React component from any of these files and it becomes a route. Layouts, loading states, and error boundaries follow the same convention.

Special Files

FilePurpose
layout.tsxShared layout for a directory
not-found.tsx404 page
error.tsxError boundary fallback (also catches loader errors)
loading.tsxInitial-load fallback

Layouts

Create a layout.tsx file in any directory to wrap all sibling and nested routes.

src/pages/
  ├── dashboard/
  │   ├── layout.tsx      # Wraps all /dashboard/* routes
  │   ├── page.tsx        # Renders at /dashboard
  │   └── settings.tsx    # Renders at /dashboard/settings
  └── page.tsx            # Renders at /

CraxRouter builds a React Router data router (createBrowserRouter + route.lazy) under the hood. Client-side navigation (<Link>, router.push()) keeps the current page mounted and visible until the next route's module (and loader, if it has one — see Data Fetching) resolves. There's no fallback flash on navigation; that's intentional.

src/pages/loading.tsx is shown only on the initial load of a location — a hard refresh or deep link, where there's no previous page to keep showing. Drop one to use your own fallback for that case; without one, Crax renders a small built-in pulse indicator that respects prefers-reduced-motion.

The layout component must render <Outlet /> to show the active child route:

// src/pages/dashboard/layout.tsx
import { Outlet } from '@crax/router'

export default function DashboardLayout() {
  return (
    <div className="flex">
      <nav className="w-64 bg-gray-800 text-white p-4">
        {/* sidebar */}
      </nav>
      <main className="flex-1 p-8">
        <Outlet />
      </main>
    </div>
  )
}

Use the Link component from @crax/router for client-side navigation with prefetching:

import { Link } from '@crax/router'

// Smart (default): prefetches on hover, focus, or pointerdown, plus a
// viewport-observation baseline for links that are never hovered
<Link to="/dashboard">Dashboard</Link>

// Foresight, predicts user intent from cursor movement (hover/pointerdown also prefetch directly)
<Link to="/pricing" prefetch="foresight">Pricing</Link>

// None, plain client-side navigation
<Link to="/terms" prefetch="none">Terms</Link>

All prefetch triggers share one dedupe set keyed by route path, so a link that's hovered, focused, and scrolled into view still only fetches its chunk once.

useRouter

Access router state and navigate programmatically:

import { useRouter } from '@crax/router'

function MyComponent() {
  const router = useRouter()

  return (
    <button onClick={() => router.push('/dashboard')}>
      Go to Dashboard
    </button>
  )
}

Programmatic prefetching

router.push() doesn't prefetch on its own. Import prefetch from @crax/router to warm a route's chunk ahead of a programmatic navigation — it dedupes against the same set the <Link> triggers use:

import { useRouter, prefetch } from '@crax/router'

async function onSubmit() {
  prefetch({ path: '/dashboard' })
  await saveForm()
  router.push('/dashboard')
}

Route Loaders

Pages can export an async loader to fetch data before the route renders. See Data Fetching for the full pattern.

getRoutes()

getRoutes() enumerates every discovered page route ({ path, filePath, isDynamic }), resolved fresh on each call — useful for SSG/prerender tooling that needs the full route list. See Static Site Generation for an example.