The code blocks to tab between. Each child becomes one tab, in source order.
Code Groups
CodeGroup folds several fenced blocks into a single tabbed panel, and the
title from each block's metastring becomes its tab label. The strip is a real
ARIA tablist: arrow keys move between tabs and wrap around at either end, Home
jumps to the first tab, End to the last, and focus follows the selection.
Every example below shows the live component first, then the exact MDX that
produced it.
Quick Start
Wrap two fenced blocks and you get two tabs. Leave a blank line above and below each fence so MDX treats them as separate blocks.
npm install next react react-dompnpm add next react react-dom<CodeGroup>
```bash title="npm"
npm install next react react-dom
```
```bash title="pnpm"
pnpm add next react react-dom
```
</CodeGroup>Adding Options
Nothing ties a group to one language. Mix fences to show the same task three ways, and each panel keeps the highlighting of its own fence. When the labels outgrow the row, the strip scrolls sideways instead of wrapping.
const greeting = 'Hello, World!'
console.log(greeting)greeting = 'Hello, World!'
print(greeting)package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}<CodeGroup>
```javascript title="JavaScript"
const greeting = 'Hello, World!'
console.log(greeting)
```
```python title="Python"
greeting = 'Hello, World!'
print(greeting)
```
```go title="Go"
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
```
</CodeGroup>Advanced
Every metastring option keeps working inside a group, so each tab can carry its own line highlighting, starting line number, word pattern, or collapse cap.
export async function loader() {
const session = await getSession()
if (!session) return redirect('/login')
return json({ user: session.user })
}export function useSession() {
const { data } = useSWR('/api/session', fetcher)
return data ?? null
}<CodeGroup>
```typescript title="Server" {2-3} /session/
export async function loader() {
const session = await getSession()
if (!session) return redirect('/login')
return json({ user: session.user })
}
```
```typescript title="Client" showLineNumbers=40
export function useSession() {
const { data } = useSWR('/api/session', fetcher)
return data ?? null
}
```
```json title="Response" expandable maxLines=4
{
"user": {
"id": "usr_01H",
"name": "Placeholder User",
"roles": ["reader"]
},
"expiresAt": "2030-01-01T00:00:00.000Z"
}
```
</CodeGroup>Options
CodeGroup takes no configuration of its own. Everything you control lives on
the blocks inside it.
childrenReactNoderequiredThe tab label is resolved from the first of these that has a value:
titlefrom the block's metastring — recommended, and the only one you set directly.- The block's
language. - The block's
aria-label, with a trailing " code block" removed. Tab N, counting from 1.
Behavior worth knowing
- Give every block in a group a distinct title. Labels are used as React keys,
so two tabs called
npmcollide. - A group holding one block renders as a plain code block with no tab strip, so wrapping a single fence costs nothing.
- An empty group renders nothing at all.
- The tab strip is horizontally scrollable, which keeps six or seven package managers usable on a phone.
- Keyboard support follows the ARIA tablist pattern:
ArrowLeftandArrowRightstep through the tabs and wrap at both ends,Homeselects the first,Endselects the last, and the newly selected tab takes focus. Only the active tab is in the tab order, soTabmoves past the whole strip in one press. - Panels stay mounted and are hidden rather than unmounted, so a collapsed block on an inactive tab keeps its state when you come back to it.
- For anything richer than code (prose, images, nested components), reach for Tabs instead.