# Not found

> A 404 with a search and a puppy: Fetch sniffs as you type, sprints off to fetch the results and comes back with a newspaper, or a stick.

A 404 page is usually a big number, an apology and a link home. This one goes and looks. A 404 is engraved into the lit stage and Fetch, an illustrated golden retriever puppy, sits in front of it with his tongue out. Start typing what you were after and he puts his nose to the ground, twitching with every key. When the typing stops he sprints off the edge of the stage, his legs switching between two strides, and your search runs; type again and the older search is aborted. He runs back in with a newspaper in his mouth and the results drop in under the field, or, if nothing matched, sits there sheepishly with a stick (“fetch() came back with a stick”) and your popular pages come back. Under the puppy it is a real recovery page: the missing address, a labelled search with a clear button, links that are links, and a way home.

## Install

```bash
npx shadcn@latest add https://hairlineui.com/r/not-found.json
```

Needs React 19, Tailwind CSS v4 and shadcn/ui theme variables (any style, light and dark). Icons from lucide-react.

- npm: `lucide-react`
- Hairline UI components it needs (the CLI installs them too): `input`, `text-morph`
- Docs and live preview: https://hairlineui.com/ui/not-found
- Registry item (source): https://hairlineui.com/r/not-found.json

## Usage

```tsx
import { NotFound } from "@/components/not-found";

<NotFound
  path={pathname}
  links={[{ title: "Pricing", href: "/pricing" }]}
  search={(query, signal) => fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal }).then((r) => r.json())}
/>
```

## As the Next.js not-found page

app/not-found.tsx renders for every unmatched route; read the path on the client and search your own index.

```tsx
"use client";
import { usePathname } from "next/navigation";
import { NotFound } from "@/components/not-found";

export default function NotFoundPage() {
  const pathname = usePathname();
  return (
    <main className="grid min-h-dvh place-items-center p-6">
      <NotFound
        path={pathname}
        links={[
          { title: "Pricing", href: "/pricing" },
          { title: "Docs", href: "/docs", hint: "Guides and reference" },
        ]}
        search={async (query, signal) => {
          const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal });
          if (!res.ok) throw new Error("Search failed");
          return res.json(); // [{ title, href, hint }]
        }}
      />
    </main>
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `search` | `(query: string, signal: AbortSignal) => Promise<NotFoundResult[]>` | – | Find pages for a query: { title, href, hint? }. Called 350ms after typing stops; aborted when a newer query starts. |
| `links` | `NotFoundResult[]` | `[]` | Shown before anyone searches and when nothing is found. |
| `path` | `string` | – | The address that wasn’t found, shown in the line under the title. |
| `homeHref` | `string` | `"/"` | Where Take me home goes. |
| `name` | `string` | `"Fetch"` | What the puppy is called in his lines. |

## States

- **lost**: Fetch sits by a 404 engraved in outline on the lit stage, breathing; “Fetch lost this page too”. Popular pages are listed under the search.
- **sniff**: Each keystroke: his nose goes to the ground with a 2px twitch, for 700ms after the last key; “Fetch has a scent”.
- **fetch**: 350ms after typing stops: he sprints off the right edge (480ms, accelerating), switching strides every 105ms; “Fetch is fetching…”.
- **found**: He runs back in (640ms, slowing), then sits with a newspaper; the results drop in 50ms apart; “Fetch found 2”.
- **stick**: No results: he comes back with a stick, “fetch() came back with a stick”, and the links return under “Try one of these”.
- **offline**: search throws: he comes back with nothing and says he couldn’t get out; the links stay.

## What it handles

Most 404 pages leave people to guess. This block shows the address that failed, searches as soon as they type, and always offers somewhere to go.

- **Search that waits**: search runs 350ms after typing stops; each newer query aborts the older request through its AbortSignal.
- **Results as links**: { title, href, hint } rows, up to five, that drop in as they arrive.
- **Never a dead end**: With nothing found (or before searching) your popular pages show instead.
- **The address**: path shows exactly what wasn’t found.
- **Failures said plainly**: If search throws, Fetch comes back empty-mouthed and says he couldn’t get out.
- **A way home**: homeHref is always there under the list.

## Replaces

a 404 page, a site search box, debounce and abort, search results, suggested links

## Where it fits

- **404 pages**: The not-found page of a site or app, with a way to find what moved.
- **Docs**: A missing docs page that searches the docs straight away.
- **Empty search**: The no-results state of any search, with a stick for comfort.
- **Old links**: Retired campaign or blog URLs that should still lead somewhere.

## Notes

- The search is a labelled input with a clear button; results and links are plain links in a list that is announced politely when it changes.
- Fetch is decoration (aria-hidden). His six poses are illustrations shipped inside fetch-art.ts as WebP data URIs (55KB of images), so the install is complete; the sniff, sprint, strides and return are code.
- One frame loop outside React moves him and sleeps when he’s still; his breathing is a compositor animation. The scene is drawn at 364 × 168 and scaled to the stage.
- With reduced motion he doesn’t run or twitch: his pose changes in place and the results appear without rising.
- Built from Input and Text morph, which the CLI installs with it. No animation library.

License: MIT (https://hairlineui.com/license#components)
