Skip to content
goodtabs UI
BrowseBrowse components

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

Without icons

Two columns

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

PropTypeDefaultDescription
items{ title: string; description: string; href: string; icon?: ReactNode }[]The tiles, in order. The description is clipped to one line.
columns2 | 33Columns 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