Skip to main content

Code Groups

3 min readStableBeginner

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
Bash
npm install next react react-dom
app/features/code/code-groups.mdx
MDX
<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.

JavaScript
JavaScript
const greeting = 'Hello, World!'
console.log(greeting)
app/features/code/code-groups.mdx
MDX
<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.

Server
TypeScript
export async function loader() {
  const session = await getSession()
  if (!session) return redirect('/login')
  return json({ user: session.user })
}
app/features/code/code-groups.mdx
MDX
<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.

childrenReactNoderequired

The code blocks to tab between. Each child becomes one tab, in source order.

The tab label is resolved from the first of these that has a value:

  1. title from the block's metastring — recommended, and the only one you set directly.
  2. The block's language.
  3. The block's aria-label, with a trailing " code block" removed.
  4. 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 npm collide.
  • 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: ArrowLeft and ArrowRight step through the tabs and wrap at both ends, Home selects the first, End selects the last, and the newly selected tab takes focus. Only the active tab is in the tab order, so Tab moves 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.
Was this page helpful?