29 / Documents
Page Navigator
A rail of page thumbnails for moving through a long document, with arrow-key navigation and a clear active page.
Showing page 2 of 3
"use client"
import * as React from "react"
import { samplePages } from "@/components/demos/document-fixtures"
import { PageNavigator } from "mischief-ui/page-navigator"
export function PageNavigatorDemo() {
const [page, setPage] = React.useState(2)
return (
<div className="grid w-full max-w-md gap-3">
<PageNavigator
className="w-full"
orientation="horizontal"
pages={samplePages}
activePage={page}
onActivePageChange={setPage}
/>
<p className="text-muted-foreground text-center text-xs">
Showing page {page} of {samplePages.length}
</p>
</div>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/page-navigatorimport { PageNavigator } from "mischief-ui/page-navigator"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react"import { FileText } from "lucide-react"import { cn } from "@/lib/utils" export type DocumentPage = { number: number src?: string label?: string} export type PageNavigatorProps = Omit< React.HTMLAttributes<HTMLElement>,Usage
export function Sidebar({ pages }: { pages: DocumentPage[] }) {
return <PageNavigator pages={pages} onActivePageChange={scrollToPage} />
}Numbering and thumbnails
Pages carry their own number rather than being counted from their position, so a navigator over pages 40 to 60 of a long document says 40 to 60. Whatever you pass is what is shown and what onActivePageChange reports.
src is optional. Without it the page still appears, as a numbered placeholder, which is what you want while thumbnails are still being rendered -- the strip keeps its full length instead of growing as images arrive and pushing the current page around.
<PageNavigator
pages={pages.map((page) => ({
number: page.number,
src: thumbnails[page.number],
label: page.heading,
}))}
activePage={current}
onActivePageChange={setCurrent}
/>Which way it runs
Vertical is the familiar side rail beside a document, and is the better choice for a long file because a tall strip holds more thumbnails at a readable size than a wide one does. Horizontal suits a short document, or a narrow screen where a side rail would take a third of the width.
renderImage lets your own image component take over -- a framework's optimised image, a signed URL that needs refreshing, a canvas you are already painting pages onto.
API
pagesDocumentPage[]Page number, optional thumbnail src, and optional label.activePage, defaultActivePagenumberThe current page, controlled or uncontrolled.onActivePageChange(page: number) => voidRuns when the page changes.orientation"vertical" | "horizontal"Rail direction. Defaults to "vertical".renderImage(props) => ReactNodeUses a framework image component for thumbnails.DocumentPage
numbernumberShown as the page number, and reported on change.srcstringThumbnail. Omit for a numbered placeholder.labelstringExtra description, such as a section heading.Accessibility
The rail is a tab list with a single tab stop. Arrow keys move between pages along the rail's orientation, Home and End jump to the ends, and focus follows selection. Pages without a thumbnail fall back to an icon and still carry their number, so the control works before any image has loaded.