Previous and next
Previous and next page cards for the end of a doc. Either side can be missing.
npx shadcn@latest add https://ui.goodtabs.co/r/prev-next.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/prev-next
Both cards side by side. They stack under sm.
Only next
The first page of a section: the next card keeps the right column.
Long titles
Titles truncate on one line and the arrows stay put.
Usage
import { PrevNext } from "@/components/ui/prev-next"
<PrevNext
prev={{ label: "Installation", href: "/docs/installation" }}
next={{ label: "Theming", href: "/docs/theming" }}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| prev | { label: string; href: string } | The page before this one. Omit on the first page. | |
| next | { label: string; href: string } | The page after this one. Omit on the last page. | |
| aria-label | string | "Pages" | Accessible name of the nav landmark. |
Source
prev-next/prev-next.tsx
import * as React from "react";
import Link from "next/link";
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
import { cn } from "@/lib/utils";
export type PrevNextPage = {
label: string;
href: string;
};
type PrevNextProps = Omit<React.ComponentProps<"nav">, "children"> & {
/** The page before this one. Omit on the first page. */
prev?: PrevNextPage;
/** The page after this one. Omit on the last page. */
next?: PrevNextPage;
};
/**
* Previous and next page cards for the end of a doc. Two bordered links side by side,
* stacked under sm. Either side can be missing.
*/
export function PrevNext({ prev, next, className, "aria-label": ariaLabel = "Pages", ...props }: PrevNextProps) {
if (!prev && !next) return null;
return (
<nav
data-slot="prev-next"
aria-label={ariaLabel}
className={cn("grid w-full gap-3 sm:grid-cols-2", className)}
{...props}
>
{prev && <PrevNextCard page={prev} direction="prev" />}
{next && <PrevNextCard page={next} direction="next" className={cn(!prev && "sm:col-start-2")} />}
</nav>
);
}
function PrevNextCard({
page,
direction,
className,
}: {
page: PrevNextPage;
direction: "prev" | "next";
className?: string;
}) {
const next = direction === "next";
return (
<Link
href={page.href}
rel={next ? "next" : "prev"}
className={cn(
"group/prev-next flex min-w-0 flex-col gap-1 rounded-lg border bg-background p-4 transition-colors duration-100 ease-out outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/50",
next ? "items-end text-right" : "items-start text-left",
className,
)}
>
<span className="font-mono text-xs tracking-wide text-muted-foreground uppercase group-hover/prev-next:text-foreground/70">
{next ? "Next" : "Previous"}
</span>
<span className="flex w-full min-w-0 items-center gap-1.5 text-sm font-medium text-foreground">
{!next && <ArrowLeftIcon aria-hidden="true" className="size-3.5 shrink-0" />}
<span className={cn("min-w-0 truncate", next && "ml-auto")}>{page.label}</span>
{next && <ArrowRightIcon aria-hidden="true" className="size-3.5 shrink-0" />}
</span>
</Link>
);
}
For AI agents
Hand your agent this page, the markdown at /components/prev-next/prompt.md, or the Copy AI prompt button above.
When to use
- At the end of a docs or guide page that sits in a sequence
- Chapters of a long form piece or a course
- Anywhere the next step is one obvious page
When not to use
- For numbered pages of a list or table, use pagination
- For moving between sibling records in an app, use icon buttons in the header
- When there is no sequence: it renders nothing without prev or next
Agent notes
- Server component with next/link, so pass real hrefs; compute prev and next from your ordered page list
- With only next, the card keeps the right column from sm up so the layout does not jump between pages
- Labels truncate to one line; keep them to the page title, not a description
- The links carry rel=prev and rel=next for crawlers and readers