Skip to content
goodtabs UI
BrowseBrowse components

Feature card

An icon, a title and a body, alone or in a grid that shares one set of hairlines.

npx shadcn@latest add https://ui.goodtabs.co/r/feature-card.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/feature-card

One calm look

Every component reads your theme tokens, so it matches the rest of the app the moment it lands.

Built for agents

Each page ships a prompt with when to use it, when not to, and the gotchas.

Installs in one line

One shadcn command copies the source into your project. No package to update.

Three features in a FeatureGrid, divided by hairlines, stacked below md.

Single card

Keyboard first

Every control is a real button or link with a visible focus ring, so it works without a mouse.

One FeatureCard carrying its own frame through className.

Four columns

Edge delivery

Served from the region nearest to each visitor.

Private by default

Nothing is public until you share a link.

Instant rollback

Every deploy keeps the last ten versions one click away.

Fair pricing

Pay for what you use, with a free tier that stays free.

columns={4}: two columns from md, four from lg.

Usage

import { FeatureCard, FeatureGrid } from "@/components/ui/feature-card"
import { ZapIcon } from "lucide-react"

<FeatureGrid>
  <FeatureCard icon={<ZapIcon />} title="Installs in one line">
    One shadcn command copies the source into your project.
  </FeatureCard>
</FeatureGrid>

Props

PropTypeDefaultDescription
iconReactNodeA 16px icon, drawn in muted-foreground.
titleReactNodeThe feature title, one short line.
childrenReactNodeThe body, one or two sentences.
columns2 | 3 | 43On FeatureGrid: columns from md up. One column below.

Source

feature-card/feature-card.tsx
import * as React from "react";
import { cn } from "@/lib/utils";

type FeatureCardProps = Omit<React.ComponentProps<"div">, "title"> & {
  /** A 16px icon, drawn in muted-foreground. */
  icon?: React.ReactNode;
  /** The feature title, one short line. */
  title: React.ReactNode;
  /** The body, one or two sentences. */
  children?: React.ReactNode;
};

/**
 * One feature: icon, title and body. Unstyled on the outside so it can sit in a FeatureGrid
 * or carry its own frame through className.
 */
export function FeatureCard({ icon, title, className, children, ...props }: FeatureCardProps) {
  return (
    <div data-slot="feature-card" className={cn("flex flex-col gap-3 bg-card p-6", className)} {...props}>
      {icon && (
        <span aria-hidden="true" className="inline-flex size-4 shrink-0 text-muted-foreground [&_svg]:size-4">
          {icon}
        </span>
      )}
      <div className="flex flex-col gap-1">
        <h3 className="m-0 text-sm leading-5 font-medium text-foreground/85">{title}</h3>
        {children && <div className="text-sm text-muted-foreground">{children}</div>}
      </div>
    </div>
  );
}

type FeatureGridProps = React.ComponentProps<"div"> & {
  /** Columns from md up. One column below. */
  columns?: 2 | 3 | 4;
};

const COLUMNS = {
  2: "md:grid-cols-2",
  3: "md:grid-cols-3",
  4: "md:grid-cols-2 lg:grid-cols-4",
} as const;

/**
 * A shared-hairline frame for FeatureCard cells: one rounded border, one hairline between every cell.
 * The frame paints the hairlines, each cell paints its own card fill.
 */
export function FeatureGrid({ columns = 3, className, ...props }: FeatureGridProps) {
  return (
    <div
      data-slot="feature-grid"
      className={cn(
        "surface-shine grid w-full gap-px overflow-hidden rounded-lg border bg-border",
        COLUMNS[columns],
        className,
      )}
      {...props}
    />
  );
}

For AI agents

Hand your agent this page, the markdown at /components/feature-card/prompt.md, or the Copy AI prompt button above.

When to use

  • A row of three or four benefits under a hero or a section heading
  • Any grid of short, equal-weight points that should read as one surface
  • A single highlighted capability inside a longer page

When not to use

  • Numbers and trends, which want the stat card
  • Tiles of different sizes, which want the bento grid
  • Long copy or images per feature, which want their own section

Agent notes

  • FeatureCard has no border of its own; inside FeatureGrid the frame draws the hairlines, alone add rounded-lg border through className
  • FeatureGrid paints bg-border and each cell paints bg-card, so every cell must stay opaque
  • Keep the body to two lines; cells in one row share a height and long copy stretches the whole row
  • Server components with no state; the icon is decorative and hidden from screen readers