Quick links
A grid of link tiles with icon, title and one line, sharing hairlines in one frame.
npx shadcn@latest add https://ui.goodtabs.co/r/quick-links.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/quick-links
Six docs tiles with icons, three columns from lg. Hover shows the arrow.
Without icons
Two columns
columns={2} for a settings page or a narrower column.
Usage
import { QuickLinks } from "@/components/ui/quick-links"
<QuickLinks
items={[
{ title: "Quickstart", description: "Deploy in five minutes.", href: "/docs/quickstart", icon: <RocketIcon /> },
{ title: "CLI", description: "Install and log in.", href: "/docs/cli", icon: <TerminalIcon /> },
]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| items | { title: string; description: string; href: string; icon?: ReactNode }[] | The tiles, in order. The description is clipped to one line. | |
| columns | 2 | 3 | 3 | Columns at the widest breakpoint. One column below sm, two from sm, three from lg. |
Source
quick-links/quick-links.tsx
import * as React from "react";
import Link from "next/link";
import { ArrowUpRightIcon } from "lucide-react";
import { cn } from "@/lib/utils";
export type QuickLink = {
title: string;
description: string;
href: string;
icon?: React.ReactNode;
};
type QuickLinksProps = Omit<React.ComponentProps<"div">, "children"> & {
/** The tiles, in order. */
items: QuickLink[];
/** Columns at the widest breakpoint. Both start at one column and go to two from sm. */
columns?: 2 | 3;
};
/**
* A grid of link tiles that share hairlines inside one rounded frame.
* Each tile has an icon, a title, a one-line description and an arrow on hover.
*/
export function QuickLinks({ items, columns = 3, className, ...props }: QuickLinksProps) {
return (
<div
data-slot="quick-links"
className={cn("w-full overflow-hidden rounded-lg border bg-card text-card-foreground", className)}
{...props}
>
<div className={cn("-mr-px -mb-px grid grid-cols-1 sm:grid-cols-2", columns === 3 && "lg:grid-cols-3")}>
{items.map((item) => (
<Link
key={`${item.href}-${item.title}`}
href={item.href}
data-slot="quick-link"
className="group relative flex flex-col gap-1 border-r border-b p-4 transition-colors duration-100 ease-out outline-none hover:bg-muted/60 focus-visible:bg-muted/60 focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset"
>
{item.icon && (
<span
aria-hidden="true"
className="mb-1 flex size-4 items-center text-muted-foreground [&_svg]:size-4 [&_svg]:shrink-0"
>
{item.icon}
</span>
)}
<span className="text-sm font-medium">{item.title}</span>
<span className="line-clamp-2 text-sm text-muted-foreground">{item.description}</span>
<ArrowUpRightIcon
aria-hidden="true"
className="absolute top-3 right-3 size-4 text-muted-foreground opacity-0 transition-opacity duration-100 ease-out group-hover:opacity-100 group-focus-visible:opacity-100"
/>
</Link>
))}
</div>
</div>
);
}
For AI agents
Hand your agent this page, the markdown at /components/quick-links/prompt.md, or the Copy AI prompt button above.
When to use
- The top of a docs home, a settings index or a help centre
- Four to nine destinations that each deserve a short description
- When the tiles should read as one calm block rather than separate cards
When not to use
- For one or two links, use arrow links in a paragraph
- When each item needs an image, a badge or more than one line of copy
- For in-page navigation, use tabs or a sidebar
Agent notes
- Server component using next/link, so pass real hrefs; it needs no client wrapper
- The frame clips the last column's right hairline and the last row's bottom hairline, so any item count looks right
- Pass a max-width through className; the grid is w-full
- Icons render at 16px in muted-foreground; any lucide icon or inline SVG using currentColor works