Skip to main content

Code Blocks

4 min readStableBeginner

Every fenced code block in an MDX page renders through the same component: Prism.js syntax highlighting, a header bar carrying the file title and the language name, line numbers down the left edge, and a copy button. You tune all of it from the metastring — the space-separated text you write after the language on the opening fence line.

Quick Start

A fence with nothing but a language identifier already gets highlighting, line numbers, a header bar, and a copy button.

JavaScript
JavaScript
async function fetchDocumentation(slug) {
  const response = await fetch(`/api/docs/${slug}`)
  return response.json()
}
app/features/code/code-blocks.mdx
MDX
```javascript
async function fetchDocumentation(slug) {
  const response = await fetch(`/api/docs/${slug}`)
  return response.json()
}
```

Adding Options

Options are space-separated and order never matters. This fence names a file and highlights the three lines that do the work.

lib/auth.ts
TypeScript
import { SignJWT } from 'jose'
 
const secret = new TextEncoder().encode(process.env.JWT_SECRET)
export async function createSession(payload) {
  return new SignJWT(payload).setProtectedHeader({ alg: 'HS256' }).sign(secret)
}
app/features/code/code-blocks.mdx
MDX
```typescript {3-5} title="lib/auth.ts"
import { SignJWT } from 'jose'
 
const secret = new TextEncoder().encode(process.env.JWT_SECRET)
export async function createSession(payload) {
  return new SignJWT(payload).setProtectedHeader({ alg: 'HS256' }).sign(secret)
}
```

Advanced

All eight options stack on one fence. The block below sets a title, highlights two ranges, starts numbering at 120, marks every SearchItem, and collapses to six visible lines with a "Show more" button.

lib/search-index.ts
TypeScript
export function buildSearchIndex(version?: string): SearchItem[] {
  const searchIndex: SearchItem[] = []
  const pages = collectPages(version)
 
  for (const page of pages) {
    const record: SearchItem = toSearchItem(page)
    searchIndex.push(record)
  }
 
  return searchIndex
}
app/features/code/code-blocks.mdx
MDX
```typescript {2,5-7} title="lib/search-index.ts" showLineNumbers=120 /SearchItem/ expandable maxLines=6
export function buildSearchIndex(version?: string): SearchItem[] {
  const searchIndex: SearchItem[] = []
  const pages = collectPages(version)
 
  for (const page of pages) {
    const record: SearchItem = toSearchItem(page)
    searchIndex.push(record)
  }
 
  return searchIndex
}
```

Options

Each option below is matched independently against the metastring, so you can write them in any order. Only the first occurrence of an option counts, and text the parser doesn't recognize is ignored rather than treated as an error.

languagestringDefault: text

The identifier straight after the opening backticks. It selects the Prism grammar and fills the language name on the right of the header bar. A fence with no identifier falls back to plain text.

title="filename"stringDefault: language name

File path shown on the left of the header bar, in double or single quotes. Covered on File title.

{lines}number[]

Lines to tint, as single numbers, ranges, or both: {1,3-5}. Covered on Line highlighting.

showLineNumbersbooleanDefault: true

Line numbers are already on for every block, so writing this only makes the intent explicit. Covered on Line numbers.

showLineNumbers=NnumberDefault: 1

Starts the gutter at line N instead of 1. Covered on Line numbers.

/pattern/string

Marks every literal occurrence of the text between the slashes. Covered on Word highlighting.

expandablebooleanDefault: false

Collapses the block behind a "Show more" button. Covered on Collapsible code.

maxLines=Nnumber

How many lines stay visible while the block is collapsed. Covered on Collapsible code.

Diff markers are the one feature that lives in the code rather than the metastring — see Diff highlighting.

Languages

The language registry holds 71 identifiers and 49 aliases, so ts, yml, sh, py, rs, tf, and dockerfile all resolve to their full names. The resolved name is what you see on the right of the header bar: ts shows as TypeScript, tf as HCL, wat as WebAssembly.

Two details are worth knowing. tsx borrows the TypeScript grammar, so JSX inside a .tsx sample is highlighted as TypeScript. And an identifier with no loaded grammar still renders: escaped, unhighlighted, with the badge and the copy button intact. An unknown language degrades quietly instead of breaking the page.

Terminal languages get their own header icon. bash, sh, shell, zsh, powershell, terminal, and console show a terminal glyph; everything else shows a file glyph.

deploy.sh
Bash
npm install
npm run build
npm start
app/features/code/code-blocks.mdx
MDX
```bash title="deploy.sh" {1,3}
npm install
npm run build
npm start
```

Copy behavior

The copy button hands over the block's original source text, not the rendered markup. Line numbers are drawn with CSS counters, so they never end up on the clipboard. Diff markers do — they are part of the source you wrote. Text pasted from a collapsed block includes the hidden lines as well, because collapsing only clips the height.

Was this page helpful?