Feature tabs
A feature section behind tabs, each panel with text, points and a visual.
npx shadcn@latest add https://ui.goodtabs.co/r/feature-tabs.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/feature-tabs
Workflow
One place for the whole review
Sort the queue in seconds
Every new issue lands in one inbox with its owner, labels and last activity already filled in.
Three tabs, each a heading and a short paragraph.
With a visual
Find any component by what it does
Type a job, such as date range or file drop, and get the right piece with its install command.
- Matches names, tags and descriptions
- Keyboard first, opens with Cmd K
- Results update as you type
Text column and a visual slot beside it from md up.
Usage
import { FeatureTabs } from "@/components/feature-tabs"
<FeatureTabs title="One place for the review" features={[{ value: "triage", label: "Triage", title: "Sort the queue", description: "Every issue lands in one inbox." }]} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| features | Feature[] | One entry per tab: value, label, title, description, optional points and visual. | |
| eyebrow | string | Small mono line above the heading. | |
| title | string | Section heading. | |
| label | string | "Features" | Accessible name for the tab row. |
| defaultValue | string | Tab open first. Defaults to the first feature. |
Source
feature-tabs/feature-tabs.tsx
"use client";
import * as React from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
type Feature = {
/** Unique id for the tab. */
value: string;
/** The tab label. Keep it to one or two words. */
label: string;
/** The heading shown when the tab is active. */
title: string;
/** One or two sentences under the heading. */
description: string;
/** Optional short points, drawn as a hairline list. */
points?: string[];
/** Optional visual for the panel: a screenshot, a frame, a demo. */
visual?: React.ReactNode;
};
type FeatureTabsProps = Omit<React.ComponentProps<typeof Tabs>, "children"> & {
/** Small mono line above the tabs. */
eyebrow?: string;
/** Section heading. */
title?: string;
/** The features, one per tab. */
features: Feature[];
/** Accessible name for the tab row. */
label?: string;
};
/**
* A feature section behind tabs. The tab row is a line tabs list; each panel is a text column
* and an optional visual. Client component because radix tabs hold the active value.
*/
export function FeatureTabs({
eyebrow,
title,
features,
label = "Features",
defaultValue,
className,
...props
}: FeatureTabsProps) {
return (
<Tabs
data-slot="feature-tabs"
defaultValue={features.some((f) => f.value === defaultValue) ? defaultValue : features[0]?.value}
className={cn("w-full gap-6", className)}
{...props}
>
{(eyebrow || title) && (
<header className="flex flex-col gap-2">
{eyebrow && <p className="m-0 font-mono text-xs tracking-wide text-muted-foreground uppercase">{eyebrow}</p>}
{title && <h2 className="m-0 text-2xl leading-tight font-medium tracking-tight text-balance">{title}</h2>}
</header>
)}
<TabsList variant="line" aria-label={label} className="w-full justify-start overflow-x-auto border-b pb-1">
{features.map((f) => (
<TabsTrigger
key={f.value}
value={f.value}
className="h-7 flex-none px-2.5 text-foreground/70 dark:text-foreground/70"
>
{f.label}
</TabsTrigger>
))}
</TabsList>
{features.map((f) => (
<TabsContent
key={f.value}
value={f.value}
className={cn(
"grid grid-cols-1 gap-6 pt-2",
f.visual && "md:grid-cols-[minmax(0,2fr)_minmax(0,3fr)] md:gap-8",
)}
>
<div className="flex flex-col gap-4 [overflow-wrap:anywhere]">
<h3 className="m-0 text-base font-medium text-foreground">{f.title}</h3>
<p className="m-0 text-sm text-muted-foreground">{f.description}</p>
{f.points && f.points.length > 0 && (
<ul className="m-0 list-none divide-y border-y p-0">
{f.points.map((p, i) => (
<li key={i} className="py-2 text-sm text-foreground/85">
{p}
</li>
))}
</ul>
)}
</div>
{f.visual && <div className="min-w-0">{f.visual}</div>}
</TabsContent>
))}
</Tabs>
);
}
export type { Feature };
For AI agents
Hand your agent this page, the markdown at /components/feature-tabs/prompt.md, or the Copy AI prompt button above.
When to use
- A product page with three to five capabilities that each need a paragraph
- When a visual or a short list explains each capability better than a grid
- Below a hero, one per page
When not to use
- For a single feature, which needs no tabs
- For steps in order, where a numbered list reads better
- For navigation between pages
Agent notes
- Client component on the shadcn Tabs line variant; keyboard arrows come from radix
- Without a visual a panel is one text column; with one it splits 2fr and 3fr from md
- Pass your own element as visual, such as a browser-frame; panels unmount when inactive
- Tab labels scroll sideways on narrow screens, keep them to one or two words