Browse the docs
BlocksNo. 97 of 109
Feature Grid
A grid of features with an icon, a title and a description under a centred section header.
Installation
pnpm dlx shadcn@latest add @corsair-ui/feature-gridNo setup needed: the CLI finds @corsair-ui in the shadcn registry directory. The files land in your project as source: edit them like your own code. More in Installation.
Examples
Default
Six features in three columns on large screens.
Two columns with links
With href, the title's link covers the whole card.
API
| Prop | Type | Default |
|---|---|---|
eyebrowSmall line above the title; null hides it. | ReactNode | "Features" |
title / descriptionThe header; the title is an h2. null hides either. | ReactNode | example copy |
featuresEach has title, and optionally id (the key), icon, description and href (links the whole card). | FeatureGridFeature[] | six examples |
columnsColumns on large screens. One on mobile and two from sm. | 2 | 3 | 4 | 3 |
Accessibility
- The features are a list; each title is an h3 under the section's h2.
- Icons are hidden from screen readers, so the title has to say what the feature is.
- With href, the title's link covers the card and the card shows the focus ring; the link is named by the title.
Compatibility
- Tailwind CSS
- 3.4 and 4, both checked in CI
- React
- 19
- Rendering
- Works in server components
Source
1 file, installed as source you own
feature-grid.tsxView on GitHubimport {
GaugeIcon,
LayersIcon,
PuzzleIcon,
ShieldCheckIcon,
SparklesIcon,
ZapIcon,
} from "lucide-react";
import type { ComponentProps, ReactNode } from "react";
import { cn } from "@/registry/default/lib/utils";
import { Card } from "@/registry/default/ui/card";
interface FeatureGridFeature {
/** Stable React key. Falls back to the index. */
id?: string;
/** Shown in a small bordered box above the title. Hidden from screen readers. */
icon?: ReactNode;
title: ReactNode;
description?: ReactNode;
/** Turns the title into a link that covers the whole card. */
href?: string;
}
type FeatureGridColumns = 2 | 3 | 4;
interface FeatureGridProps extends Omit<ComponentProps<"section">, "title"> {
/** Small line above the title; pass `null` to hide it. */
eyebrow?: ReactNode;
/** Section title, rendered in an `<h2>`. */
title?: ReactNode;
description?: ReactNode;
features?: FeatureGridFeature[];
/** Columns on large screens. Always one on mobile and two from `sm`. */
columns?: FeatureGridColumns;
}
// Full class names, so Tailwind finds them in the source.
const COLUMNS: Record<FeatureGridColumns, string> = {
2: "sm:grid-cols-2",
3: "sm:grid-cols-2 lg:grid-cols-3",
4: "sm:grid-cols-2 lg:grid-cols-4",
};
const defaultFeatures: FeatureGridFeature[] = [
{
id: "fast",
icon: <ZapIcon />,
title: "Fast by default",
description: "Pages load in milliseconds, with caching and prefetching set up for you.",
},
{
id: "secure",
icon: <ShieldCheckIcon />,
title: "Secure from day one",
description: "Single sign-on, audit logs and encryption at rest come with every plan.",
},
{
id: "composable",
icon: <LayersIcon />,
title: "Composable",
description: "Start with the pieces you need and add more as your product grows.",
},
{
id: "integrations",
icon: <PuzzleIcon />,
title: "Integrations",
description: "Connect the tools your team already uses in a few clicks.",
},
{
id: "insights",
icon: <GaugeIcon />,
title: "Real-time insights",
description: "See how people use your product as it happens, not a day later.",
},
{
id: "automation",
icon: <SparklesIcon />,
title: "Smart automation",
description: "Hand repetitive work to workflows that run on their own.",
},
];
/**
* A grid of features, each with an icon, a title and a short description,
* under an optional centred header (eyebrow, `<h2>` title, description).
* `<FeatureGrid />` renders six example features.
*
* Pass `features` to replace them and `columns` (2, 3 or 4) for the number
* of columns on large screens. With `href`, a feature's title becomes a link
* stretched over the whole card, and the card shows the focus ring.
*
* Accessibility: the features are a list; each title is an `<h3>` under the
* section's `<h2>`. Icons are decorative and hidden from screen readers, so
* the title has to say what the feature is.
*
* @example
* <FeatureGrid
* title="Everything you need"
* columns={2}
* features={[
* { icon: <ZapIcon />, title: "Fast", description: "Loads in milliseconds.", href: "#speed" },
* { icon: <LockIcon />, title: "Private", description: "Your data stays yours." },
* ]}
* />
*/
function FeatureGrid({
eyebrow = "Features",
title = "Everything your team needs to ship",
description = "A focused set of tools that work together, so you can spend your time on the product instead of the plumbing.",
features = defaultFeatures,
columns = 3,
className,
...props
}: FeatureGridProps) {
const hasHeader = eyebrow != null || title != null || description != null;
return (
<section data-slot="feature-grid" className={cn("py-16 sm:py-24", className)} {...props}>
<div data-slot="feature-grid-container" className="mx-auto w-full max-w-6xl px-4 sm:px-6">
{hasHeader ? (
<div data-slot="feature-grid-header" className="mx-auto max-w-2xl text-center">
{eyebrow != null ? (
<p
data-slot="feature-grid-eyebrow"
className="text-muted-foreground text-sm font-medium"
>
{eyebrow}
</p>
) : null}
{title != null ? (
<h2
data-slot="feature-grid-title"
className={cn(
"text-3xl font-semibold tracking-tight text-balance sm:text-4xl",
eyebrow != null && "mt-2"
)}
>
{title}
</h2>
) : null}
{description != null ? (
<p
data-slot="feature-grid-description"
className="text-muted-foreground mt-4 text-lg text-pretty"
>
{description}
</p>
) : null}
</div>
) : null}
<ul
data-slot="feature-grid-list"
data-columns={columns}
className={cn("grid gap-6", COLUMNS[columns], hasHeader && "mt-12")}
>
{features.map((feature, index) => (
<li key={feature.id ?? index} data-slot="feature-grid-item" className="flex">
<Card
data-linked={feature.href ? true : undefined}
className={cn(
"relative w-full gap-4 px-6",
feature.href &&
"hover:bg-muted/50 has-[a:focus-visible]:border-ring has-[a:focus-visible]:ring-ring/50 transition-[color,background-color,border-color,box-shadow] has-[a:focus-visible]:ring-[3px] motion-reduce:transition-none"
)}
>
{feature.icon != null ? (
<div
aria-hidden="true"
data-slot="feature-grid-icon"
className="bg-muted flex size-10 shrink-0 items-center justify-center rounded-md border [&_svg]:size-5 [&_svg]:shrink-0"
>
{feature.icon}
</div>
) : null}
<div className="flex flex-col gap-2">
<h3 data-slot="feature-grid-item-title" className="font-semibold">
{feature.href ? (
<a
href={feature.href}
className="outline-none after:absolute after:inset-0 after:rounded-xl"
>
{feature.title}
</a>
) : (
feature.title
)}
</h3>
{feature.description != null ? (
<p
data-slot="feature-grid-item-description"
className="text-muted-foreground text-sm text-pretty"
>
{feature.description}
</p>
) : null}
</div>
</Card>
</li>
))}
</ul>
</div>
</section>
);
}
export { FeatureGrid, type FeatureGridColumns, type FeatureGridFeature, type FeatureGridProps };