Skip to content
goodtabs UI
BrowseBrowse components

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

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

PropTypeDefaultDescription
featuresFeature[]One entry per tab: value, label, title, description, optional points and visual.
eyebrowstringSmall mono line above the heading.
titlestringSection heading.
labelstring"Features"Accessible name for the tab row.
defaultValuestringTab 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