Skip to content
goodtabs UI
BrowseBrowse components

Feature grid

Feature columns divided by hairlines, with a plain title and a grey body.

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

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

  • Installs in one line

    One command copies the source into your project.

  • Tokens, not hex

    Every color is a theme token, so dark mode comes free.

  • Keyboard first

    Focus states and arrow keys work before you style anything.

Three features, no heading.

With a heading

Built for the way you ship

Small parts that compose, with no hidden runtime.

  • Server components

    Static markup by default, client code only where state lives.

  • Container aware

    Layouts respond to their column, not the window.

  • Typed props

    Every slot has a type and a one line comment.

A section heading and lead above the columns.

Six features

Everything a settings page needs

  • Profile forms

    Name, avatar and email with inline validation.

  • API keys

    Create, copy once, revoke. Masked by default.

  • Billing

    Plan, usage and invoices in one table.

  • Members

    Invite by email and set a role per person.

  • Audit log

    Every change with who, what and when.

  • Danger zone

    Delete flows that ask twice and say what goes.

Six features wrap to two rows of three.

Usage

import { FeatureGrid } from "@/components/feature-grid"

<FeatureGrid
  title="Built for the way you ship"
  features={[
    { title: "Installs in one line", description: "One command copies the source." },
    { title: "Tokens, not hex", description: "Dark mode comes free." },
    { title: "Keyboard first", description: "Focus states from the start." },
  ]}
/>

Props

PropTypeDefaultDescription
titleReactNodeOptional section heading.
descriptionReactNodeOptional grey lead under the heading.
features{ title: string; description: string }[]The features, each a title and a one line description.

Source

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

export type Feature = { title: string; description: string };

type FeatureGridProps = Omit<React.ComponentProps<"section">, "title"> & {
  /** Optional section heading above the columns. */
  title?: React.ReactNode;
  /** Optional grey lead under the heading. */
  description?: React.ReactNode;
  /** The features. Three reads best; six wraps to two rows. */
  features: Feature[];
};

/**
 * Feature columns divided by hairlines: a 14px title, a grey body, no icons.
 * Three columns from the container's md width, one below. Server component.
 */
export function FeatureGrid({ title, description, features, className, ...props }: FeatureGridProps) {
  return (
    <section className={cn("@container w-full py-8 @2xl:py-16", className)} {...props}>
      <div className="mx-auto w-full max-w-6xl px-4 @2xl:px-6">
        {(title || description) && (
          <div className="mb-8 flex max-w-2xl flex-col gap-3 @2xl:mb-12">
            {title && (
              <h2 className="m-0 text-xl leading-tight font-medium tracking-tight text-balance @2xl:text-3xl">
                {title}
              </h2>
            )}
            {description && <p className="m-0 text-sm text-muted-foreground @2xl:text-base">{description}</p>}
          </div>
        )}
        <ul className="m-0 grid list-none gap-y-6 p-0 @2xl:grid-cols-3 @2xl:gap-y-10">
          {features.map((f, i) => (
            <li
              key={f.title}
              className={cn(
                "flex flex-col gap-1.5",
                i > 0 && "border-t pt-6 @2xl:border-t-0 @2xl:pt-0",
                i % 3 !== 0 && "@2xl:border-l @2xl:pl-6",
                i % 3 !== 2 && "@2xl:pr-6",
              )}
            >
              <h3 className="m-0 text-sm font-medium text-foreground/85">{f.title}</h3>
              <p className="m-0 text-sm text-muted-foreground">{f.description}</p>
            </li>
          ))}
        </ul>
      </div>
    </section>
  );
}

For AI agents

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

When to use

  • Three or six equal-weight benefits under a hero
  • When the copy is short and icons would only decorate
  • Any section that should read as quiet text, not cards

When not to use

  • Cards with icons or borders, which want the feature card
  • Mixed emphasis, which wants the bento
  • Long copy or an image per feature, which wants the sticky list

Agent notes

  • Three columns from the container's @2xl width, one column with top hairlines below
  • Hairlines are border-l on columns 2 and 3 of each row; keep the count a multiple of three
  • Titles are 14px foreground/85, bodies muted; there is no icon slot by design
  • Server component with no state