Code Group (Multi-Runtime Tabs)
<CodeGroup> shows a single concept across synchronized tabs instead of stacking three separate code blocks. It is built for inherently multi-surface ecosystems -- a Solana feature is an on-chain Rust program and a TypeScript client and a CLI invocation, and a reader wants to flip between them in place rather than scroll past all three.
The reader's active-language choice is remembered and synchronized across every group on the page (and site) for the session, so choosing "TypeScript" once keeps every group showing TypeScript.
Reader default tab (optional feature). When the site enables default-tab preferences, every distinct group appears in Settings → "Code example defaults" with a dropdown to pick the tab it opens on by default. That choice is saved to the reader's device, and to their account when signed in. The per-session sync above still applies but never overwrites the saved default. Authors do nothing extra -- any
<CodeGroup>you write is picked up automatically.
Use Cases
- Solana -- the same feature as an on-chain Rust program, a TypeScript client, and a CLI command, side by side.
- Node.js -- CommonJS vs ESM, or plain JS vs TypeScript.
- Rust --
cargovs rawrustc, or sync vs async variants of an API. - Any doc where one idea has multiple equivalent surfaces and stacking blocks reads badly.
How to Author
Wrap two or more fenced code blocks in <CodeGroup> and pass labels as a comma-separated string, in the same order as the blocks. Leave a blank line around each fenced block.
<CodeGroup labels="Rust, TypeScript client, CLI">
```rust
// on-chain program
```
```ts
// client
```
```bash
solana balance ADDRESS
```
</CodeGroup>Always use the string form (
labels="A, B, C"). This repo renders MDX throughnext-mdx-remote/rsc, which does not pass JSX expression attributes -- solabels={["A", "B"]}(and any other{...}prop) is silently dropped and every tab falls back toTab 1,Tab 2, and so on. For the same reason, opt out of syncing with the stringsync="false", neversync={false}. Keep commas out of a label, since the string is split on commas.
Any block without a matching label falls back to Tab 1, Tab 2, and so on.
Live Example: Solana (the killer case)
The same "read an account balance" feature across all three Solana surfaces. Pick a tab -- the other groups on this page follow your choice.
// programs/balance/src/lib.rs -- on-chain Anchor program
use anchor_lang::prelude::*;
declare_id!("Ba1anceReader1111111111111111111111111111111");
#[program]
pub mod balance {
use super::*;
pub fn read(ctx: Context<Read>) -> Result<()> {
let lamports = ctx.accounts.wallet.lamports();
msg!("balance: {} lamports", lamports);
Ok(())
}
}
#[derive(Accounts)]
pub struct Read<'info> {
/// CHECK: read-only balance lookup
pub wallet: AccountInfo<'info>,
}// client.ts -- TypeScript client (@solana/web3.js)
import { Connection, PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
const connection = new Connection("https://api.mainnet-beta.solana.com");
const wallet = new PublicKey("So11111111111111111111111111111111111111112");
const lamports = await connection.getBalance(wallet);
console.log(`balance: ${lamports / LAMPORTS_PER_SOL} SOL`);# CLI -- Solana command line
solana balance So11111111111111111111111111111111111111112 \
--url mainnet-betaLive Example: Node.js (CJS vs ESM)
// index.cjs
const { readFile } = require("node:fs/promises");
async function main() {
const pkg = await readFile("package.json", "utf8");
console.log(JSON.parse(pkg).name);
}
main();// index.mjs
import { readFile } from "node:fs/promises";
const pkg = await readFile("package.json", "utf8");
console.log(JSON.parse(pkg).name);Live Example: Rust (sync vs async, sync="false")
This group uses sync="false" because "sync vs async" is a one-off distinction that should not hijack the site-wide language preference set by the groups above.
// blocking read with std
use std::fs;
fn main() -> std::io::Result<()> {
let contents = fs::read_to_string("Cargo.toml")?;
println!("{}", contents.lines().count());
Ok(())
}// async read with tokio
use tokio::fs;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let contents = fs::read_to_string("Cargo.toml").await?;
println!("{}", contents.lines().count());
Ok(())
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
labels | comma-separated string (e.g. "A, B, C") | Tab N | Tab text, in the same order as the code blocks. Use the string form -- expression attributes like labels={[...]} are dropped by the MDX renderer. |
sync | string ("false" to disable) | on | Remember the choice and sync it with other groups sharing a label. Set sync="false" for one-off tab sets. |
Gotchas
- Leave blank lines around each fenced code block inside
<CodeGroup>, or MDX will not parse them as separate blocks. - Label order matters -- labels map to blocks positionally, not by language.
- Syncing is by label text (case-insensitive). A group only follows a shared choice if it has a tab with that exact label; otherwise it keeps its own selection.
- Set
sync="false"(the string, notsync={false}) for distinctions that are not a language choice (sync/async, before/after) so they do not overwrite the reader's preferred language. - Use string attributes only.
next-mdx-remote/rscdrops JSX expression attributes (labels={[...]},sync={false}), so the plain-string formslabels="A, B, C"andsync="false"are the only reliable syntax.
Alternatives
- Three stacked code blocks -- simplest, but reads poorly and forces scrolling past irrelevant surfaces.
- A generic
<Tabs>component -- works for arbitrary content, but does not give code blocks first-class treatment (per-tab copy button, shared language memory, monospace-aware layout). - Separate pages per runtime -- appropriate when the surfaces diverge substantially, but splits one concept across the navigation.
Related
- Tabs -- the general-purpose tabbed content component.