Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions components/Tabs.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { useState } from 'react';
import React, { useState } from 'react';
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file now uses a default React import mainly for React.Children/React.isValidElement, but other components in this repo typically import named APIs from react (e.g., components/Accordion.js:1). To stay consistent and enable tree-shaking, consider importing Children/isValidElement/useState as named imports instead of import React, ....

Copilot uses AI. Check for mistakes.

export function TabPanels({ tabs = [] }) {
const [active, setActive] = useState(tabs[0]?.id);
const activeTab = tabs.find((tab) => tab.id === active) || tabs[0];
export function Tab({ children }) {
return children;
}

export function TabPanels({ children }) {
const tabs = React.Children.toArray(children).filter(
(child) => React.isValidElement(child) && child.props.title != null,
);
const [activeIndex, setActiveIndex] = useState(0);

Comment on lines +7 to 12
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TabPanels derives tabs from children every render, but activeIndex is initialized once and can become out-of-range if the set/order of children changes (e.g., conditional rendering). That can leave no tab selected and show an empty panel. Consider syncing/clamping activeIndex whenever the computed tabs.length changes (or when the active tab is removed).

Copilot uses AI. Check for mistakes.
if (!tabs.length) {
return null;
Expand All @@ -11,22 +17,24 @@ export function TabPanels({ tabs = [] }) {
return (
<div className="my-6 overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div className="flex flex-wrap gap-1 border-b border-slate-200 p-2 dark:border-slate-800">
{tabs.map((tab) => (
{tabs.map((tab, index) => (
<button
key={tab.id}
key={tab.props.title}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

key={tab.props.title} is not guaranteed to be unique/stable (duplicate titles will cause React key collisions and incorrect reuse). Prefer using the element’s existing key (from React.Children.toArray) or another unique identifier rather than the title text.

Suggested change
key={tab.props.title}
key={tab.key ?? index}

Copilot uses AI. Check for mistakes.
type="button"
onClick={() => setActive(tab.id)}
onClick={() => setActiveIndex(index)}
className={`rounded-md px-3 py-1.5 text-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 ${
tab.id === active
index === activeIndex
? 'bg-blue-600 text-white'
: 'text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-800'
}`}
>
{tab.label}
{tab.props.title}
</button>
))}
Comment on lines 18 to 33
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tab UI is rendered as a set of buttons without tab semantics. For accessibility, consider adding role="tablist" on the container, role="tab" + aria-selected on each button, and role="tabpanel" on the content region (with proper id/aria-controls wiring) so screen readers can interpret this as a tab interface.

Copilot uses AI. Check for mistakes.
</div>
<div className="p-4 text-sm leading-6 text-slate-700 dark:text-slate-200">{activeTab?.content}</div>
<div className="p-4 text-sm leading-6 text-slate-700 dark:text-slate-200">
{tabs[activeIndex]?.props.children}
</div>
</div>
);
}
Expand Down
3 changes: 2 additions & 1 deletion components/mdx-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import PresetSearch from './PresetSearch';
import SearchBar from './SearchBar';
import Sidebar from './Sidebar';
import Steps from './Steps';
import Tabs, { TabPanels } from './Tabs';
import Tabs, { Tab, TabPanels } from './Tabs';
import Table, { Caption, TBody, TD, TH, THead, TR } from './WikiTable';

const mdxComponents = {
Expand Down Expand Up @@ -54,6 +54,7 @@ const mdxComponents = {
THead,
TR,
Tabs,
Tab,
TabPanels,
caption: (props) => <Caption {...props} />,
pre: (props) => <CodeBlock {...props} />,
Expand Down
51 changes: 28 additions & 23 deletions content/guides/components/tabs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,43 @@ description: Tabbed content UI for grouped alternatives.

# Tabs

`Tabs` exports `TabPanels` for rendering tab groups.
`TabPanels` renders a tab group. Each `Tab` child represents one tab, with its `title` displayed as the tab button label. Tab content can be any HTML or Markdown.

## Props (`TabPanels`)
## Props

### `TabPanels`

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `tabs` | `Array<{ id: string, label: string, content: ReactNode }>` | Yes | Tab definitions. |
| `children` | `Tab` elements | Yes | One or more `Tab` components. |

### `Tab`

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | `string` | Yes | Label shown on the tab button. |
| `children` | `ReactNode` | Yes | Content rendered when the tab is active. |

## Live examples

<TabPanels
tabs={[
{ id: 'js', label: 'JavaScript', content: 'npm run dev' },
{ id: 'pnpm', label: 'pnpm', content: 'pnpm dev' },
{ id: 'bun', label: 'bun', content: 'bun run dev' },
]}
/>

<TabPanels
tabs={[
{ id: 'docs', label: 'Docs', content: 'Markdown + MDX content pages.' },
{ id: 'config', label: 'Config', content: 'JSON files for navigation and layout behavior.' },
]}
/>
<TabPanels>
<Tab title="JavaScript">npm run dev</Tab>
<Tab title="pnpm">pnpm dev</Tab>
<Tab title="Bun">bun run dev</Tab>
</TabPanels>

<TabPanels>
<Tab title="Docs">Markdown + MDX content pages.</Tab>
<Tab title="Config">JSON files for navigation and layout behavior.</Tab>
</TabPanels>

## Code

```mdx
<TabPanels
tabs={[
{ id: 'js', label: 'JavaScript', content: 'npm run dev' },
{ id: 'pnpm', label: 'pnpm', content: 'pnpm dev' },
]}
/>
<TabPanels>
<Tab title="JavaScript">npm run dev</Tab>
<Tab title="pnpm">pnpm dev</Tab>
<Tab title="Bun">bun run dev</Tab>
</TabPanels>
```

Loading