MDX Notes
Build an accessible Tabs component for MDX from scratch — keyboard nav, ARIA, controlled/uncontrolled modes, and localStorage persistence
Tabs show up everywhere in docs. "Show me the npm command. No wait, I use yarn." Or: "Give me the JavaScript version. Actually, TypeScript." Tabs let readers pick what's relevant without scrolling past stuff they don't care about.
Why Tabs in MDX?
You constantly need to show the same idea in different contexts — install commands for three package managers, code in multiple languages, platform-specific instructions. Without tabs, you'd stack all those variants vertically and make readers scroll past four versions of the same thing.
Tabs solve this. One spot, multiple views, reader picks.
Component Source Code
Here's the full component. It handles horizontal/vertical layout, controlled and uncontrolled modes, keyboard navigation, and localStorage persistence:
Basic Usage in MDX
Drop this into any MDX file:
import { Tabs, TabList, Tab, TabPanel } from '../components/Tabs';
# Installation Guide
<Tabs defaultValue="npm">
<TabList>
<Tab value="npm">npm</Tab>
<Tab value="yarn">yarn</Tab>
<Tab value="pnpm">pnpm</Tab>
</TabList>
<TabPanel value="npm">npm install @mdx-js/react @mdx-js/loader ``` </TabPanel>
<TabPanel value="yarn">
pnpm add @mdx-js/react @mdx-js/loader
</TabPanel>
</Tabs>Code Language Tabs
This is the most common docs pattern — same logic in multiple languages:
<Tabs defaultValue="js" persist="language-preference">
<TabList>
<Tab value="js" icon="🟨">JavaScript</Tab>
<Tab value="ts" icon="🔷">TypeScript</Tab>
<Tab value="py" icon="🐍">Python</Tab>
</TabList>
<TabPanel value="js">function greet(name) { return Hello, ${name}!; }
console.log(greet('World')); ``` </TabPanel>
<TabPanel value="ts">
function greet(name: string): string {
return `Hello, ${name}!`;
}
console.log(greet('World'));
```
</TabPanel>
<TabPanel value="py">def greet(name: str) -> str: return f"Hello, {name}!"
print(greet("World"))
</TabPanel>
</Tabs>Controlled Tabs
Sometimes you need to drive the tabs from outside — say from a dropdown or URL param:
Vertical Tabs
Want sidebar-style navigation? Pass orientation="vertical":
<Tabs defaultValue="general" orientation="vertical">
<TabList>
<Tab value="general">General</Tab>
<Tab value="security">Security</Tab>
<Tab value="notifications">Notifications</Tab>
<Tab value="billing">Billing</Tab>
</TabList>
<TabPanel value="general">
General settings panel with account configuration options.
</TabPanel>
<TabPanel value="security">
Security settings including 2FA, password policies, and sessions.
</TabPanel>
<TabPanel value="notifications">
Notification preferences for email, push, and in-app alerts.
</TabPanel>
<TabPanel value="billing">
Billing information, subscription plans, and invoices.
</TabPanel>
</Tabs>Persistent Tab State
The persist prop saves the user's selection to localStorage. This is great for language tabs — once someone picks "TypeScript," every code tab on the site remembers that:
{/* All tabs with the same persist key stay in sync */}
<Tabs defaultValue="ts" persist="code-language">
<TabList>
<Tab value="js">JavaScript</Tab>
<Tab value="ts">TypeScript</Tab>
</TabList>
<TabPanel value="js">JavaScript example here...</TabPanel>
<TabPanel value="ts">TypeScript example here...</TabPanel>
</Tabs>Props API Documentation
Tabs Props
| Prop | Type | Default | Description | |
|---|---|---|---|---|
defaultValue | string | '' | Initially active tab (uncontrolled mode) | |
value | string | undefined | Active tab value (controlled mode) | |
onChange | (value: string) => void | undefined | Callback when active tab changes | |
orientation | `'horizontal' \ | 'vertical'` | 'horizontal' | Tab layout direction |
persist | string | undefined | localStorage key for persisting selection | |
className | string | '' | Additional CSS classes |
Tab Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | required | Unique identifier for this tab |
children | ReactNode | required | Tab label content |
icon | ReactNode | undefined | Icon displayed before the label |
className | string | '' | Additional CSS classes |
TabPanel Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | required | Must match the corresponding Tab's value |
children | ReactNode | required | Panel content (shown when tab is active) |
className | string | '' | Additional CSS classes |
Styling with CSS
If you'd rather use CSS modules instead of Tailwind:
Keyboard Navigation
This component follows the WAI-ARIA Tabs pattern. Here's what keyboard users get:
| Key | Action |
|---|---|
ArrowRight / ArrowDown | Move focus to next tab (wraps around) |
ArrowLeft / ArrowUp | Move focus to previous tab (wraps around) |
Home | Move focus to first tab |
End | Move focus to last tab |
Enter / Space | Activate focused tab (handled by click) |
Tab | Move focus into the active panel, then to next focusable element |
Accessibility Notes
Here's what's happening under the hood for screen readers:
role="tablist"— Tells assistive tech "this is a set of tabs."
role="tab"— Each button is identified as a tab trigger.
role="tabpanel"— Each content area is linked to its trigger.
aria-selected— The active tab saystrue, the rest sayfalse.
aria-controls/aria-labelledby— These wire up the relationship between each tab and its panel. Screen readers announce "Tab controls panel-npm" etc.
aria-orientation— Tells screen readers which arrow keys to suggest (left/right for horizontal, up/down for vertical).
tabIndexmanagement — Only the active tab getstabIndex={0}. Inactive ones get-1. This is the "roving tabindex" pattern — one Tab press jumps in, another jumps out.
- Panel
tabIndex={0}— The panel itself is focusable so keyboard users can Tab into the content after selecting a tab.
Advanced: Synced Tab Groups
Want all code tabs on a page to switch together? Here's a hook that syncs multiple tab groups through localStorage + an in-memory listener map:
Summary
Tabs are one of the trickier interactive components you'll build for MDX. There's real complexity here — accessibility, keyboard nav, state persistence, controlled vs. uncontrolled modes. But the compound component pattern (Tabs > TabList > Tab + TabPanel) gives authors a clean API while context handles the wiring behind the scenes.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Building a Tabs Component.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this MDX Tutorial topic.
Search Terms
mdx-tutorial, mdx tutorial, mdx, tutorial, components, tabs, component, building a tabs component
Related MDX Tutorial Topics