Code Blocks

Code blocks are the backbone of technical documentation. A shell command, a configuration file, a function body, a diff — all of them are code blocks in disguise. Squeezed into inline code instead, readers could neither copy them nor see the indentation, and the paragraph would degenerate into a wall of monospace text. A code block solves exactly that: it presents a chunk of content verbatim, without parsing.

Markdown gives you two ways to write one. The fenced code block is the modern form, delimited by three backticks or three tildes, and it is the one you should reach for by default. The indented code block is the older form, produced by indenting content four spaces, and it survives mainly for compatibility with documents written before fences existed. This page explains how each one works, why the fenced form wins in almost every situation, and what goes wrong when a fence is not closed properly.

Fenced Code Blocks

A fenced code block opens with a run of at least three backticks and closes with a run of at least as many backticks on a line of its own. Everything between the two markers is content, and nothing inside is interpreted as Markdown:

```bash
pnpm install
pnpm build
```

```js
function add(a, b) {
  return a + b;
}
```
<pre><code class="language-bash">pnpm install
pnpm build
</code></pre>
<pre><code class="language-js">function add(a, b) {
  return a + b;
}
</code></pre>

Two details in that output deserve a pause. The HTML element is pre wrapping code: pre preserves the line breaks, while code supplies the monospace font and the semantic “this is source code” meaning that screen readers announce. And the word following the opening backticks became a class name — class="language-bash" and class="language-js". The Markdown parser does nothing else with it: it does not know what Bash is, it simply took the first word of the info string and pasted it onto the element so a syntax highlighter downstream can pick it up. A fence with no language at all is perfectly valid too — it renders as a bare <pre><code> element — but you lose the class name, and with it the chance at highlighting.

Longer Fences for Nested Backticks

A fence is three backticks by convention, not by rule. The real rule is that the closing fence must be at least as long as the opening one, and that single sentence solves the most common nesting problem: writing a document about Markdown, where the content you want to display contains backticks of its own. If the outer fence is three backticks and the content contains a run of three backticks, the parser cannot tell where the content ends and the fence closes; it closes at the first run it sees, and the rest of your example scatters across the page as a stray empty code block. The fix is to make the outer fence longer than anything it contains — four backticks on the outside, and the inner three are safely just text:

````markdown
```js
const a = 1;
```

The result is a single <pre><code class="language-markdown"> element whose content is the literal text js … . Notice the asymmetry that makes this work: a closing fence of four backticks can close a fence that opened with three, but a closing fence of three backticks cannot close one that opened with four. Lengths only ever need to grow, never shrink. Three tildes open a fence exactly the way three backticks do, and the two are interchangeable; the difference is that a tilde fence gives you a free pass on backticks, since backticks inside a tilde fence are always literal text and you never have to count them. That makes ~~~ the cleaner choice when the content is itself Markdown containing fences, because there is no counting exercise to get wrong. Backtick fences remain the more familiar default and the better choice for ordinary code.

The Info String

Everything between the opening fence and the end of that line is the info string. The parser takes its first word as the language and keeps the rest as additional metadata. This is how documentation themes attach file names, line ranges, and titles without inventing new Markdown syntax:

```js title="utils.js"
export const noop = () => {};
```

Here the title="utils.js" part produced no visible effect in plain CommonMark, and that is correct — only the first word, js, was consumed, and the rendered element still carries class="language-js". Themes such as VitePress, Docusaurus, and many documentation generators read the remaining words themselves, which is why a fence can carry a filename, a diff marker, or a highlighted line range. Because those extra words are renderer-specific, treat them as a progressive enhancement: the code must still read correctly in a renderer that ignores them entirely.

Indented Code Blocks, and How the Two Forms Compare

The older form produces a code block by indenting the content four spaces, or one tab. No marker characters are involved:

Paragraph

    const a = 1;

The result is <p>Paragraph</p> followed by a <pre><code>const a = 1;</code></pre> element — structurally identical to the fenced version, but with no class attribute on the code element. An indented block carries no info string at all, so there is no language for a highlighter to act on. The indented code block can never be highlighted. That single limitation, combined with the indentation fragility described below, is why the fenced form replaced it. Written the two different ways, the same two lines of content render as <pre><code class="language-js">const a = 1;</code></pre> and <pre><code>const a = 1;</code></pre> respectively — nearly the same page, but only one of them is styled.

AspectFenced code blockIndented code block
Opening markerThree backticks or three tildesFour spaces of indentation
Language identifierYes, taken from the info stringNo
Syntax highlightingPossibleImpossible
Can interrupt a paragraphYesNo

The row about interrupting a paragraph surprises people. A fence may start immediately after a line of prose, with no blank line in between, and the parser will break the paragraph and open the block. An indented block cannot, because four spaces of indentation on the line right after a paragraph is just a lazy continuation of that paragraph, so the text stays a paragraph. Written as source, the first pair is a paragraph, a code block, and a paragraph again, while the second pair collapses into a single paragraph holding all three lines. When you write an indented block, the blank line before it is mandatory.

What a Fence Preserves

Inside a fence, the content is literal. Blank lines stay blank, indentation stays at exactly the column you typed it, and trailing spaces survive. This is the property that makes fences the only reliable way to show code whose formatting carries meaning:

```python
def greet(name):
    return f"Hello, {name}"


greet("world")
```

The four spaces before return and the two blank lines between the function and the call survive into the rendered output untouched: nothing is stripped, nothing is reflowed, and the block is copied out of the page byte for byte. The same guarantee lets you show a snippet that is itself indented — a method inside a class, a YAML document nested under a key — without the surrounding page’s own indentation bleeding in. A fence resets the baseline to column zero, whereas an indented block has to fight the indentation of whatever list or quote encloses it.

Code Blocks Inside Lists and Quotes

A code block can live inside a list item. The rule is to indent the fence by the number of columns the list item’s content starts at. Since - occupies two columns, indenting by two spaces is correct:

- List item

  ```js
  const a = 1;
  ```

This is exactly where indented code blocks fall apart. The list item’s content starts two columns in, so an indented block inside it needs six spaces before it — with only four, the text collapses back into an ordinary paragraph instead of becoming code, and with more than six the code block picks up leading spaces it should not have. The fenced form does not have this problem: the fence marker is what defines the block, so it works at any indent that is still inside the list item, and the columns never matter. Inside a blockquote the rule is different again: every line of the block takes the > prefix, including the fence markers and the code itself.

> ```js
> const a = 1;
> ```

Common Pitfalls

Written asWhat actually happensWhy
Three backticks to open, four to closeA closed code blockThe closing fence only needs to be at least as long
Four backticks to open, three to closeThe block runs to the end of the documentA closing fence may not be shorter
An opening fence with no closing fenceEverything after it becomes codeAn unclosed fence swallows the rest of the file
An indented block inside a list itemA paragraph, or code at the wrong offsetIndent to the list item’s content column, plus four

Exercises

Rewrite the following plain text as Markdown. Requirements: represent the two command sections as fenced code blocks with correct language identifiers; the second section contains three backticks of its own, so the fence around it must be long enough to contain them; and place the whole thing inside a single unordered list item.

Step one, install the dependencies:
pnpm install

Step two, publish:

pnpm publish

Fenced code blocksLanguage identifierNested backticks

Approach:

A fenced code block opens and closes with three backticks, and the closing fence must sit on a line of its own. The hard part is the second section: its content already contains three backticks, so an outer fence of the same length would close at the first run inside the content and the example would fall apart. The fix is to make the outer fence four backticks long. Once the whole thing sits in a list item, the indentation has to match the list item’s content column.

Steps:

  1. Use four backticks for every outer fence, so a three-backtick run inside the content can never be mistaken for the closing marker.
  2. Give the first block the identifier bash; its content is the single line pnpm install.
  3. Give the second block the identifier bash as well; its content is pnpm publish wrapped in three backticks, which are now just text because the outer fence is longer.
  4. Put everything under one unordered list item. The marker - takes two columns, so every following line is indented by exactly two spaces. Check the result: rendering shows a single list item containing two code blocks, and the second block still shows its three backticks intact instead of being cut in half.

Answer:

- Step one, install the dependencies:

  ````bash
  pnpm install
  ````

  Step two, publish:

  ````bash
  ```
  pnpm publish
  ```
  ````

Summary

You should now be able to write a fenced code block that opens and closes correctly, to lengthen the fence when the content contains backticks, to pick a language identifier that a highlighter will recognize, and to explain exactly why an indented code block can never be highlighted. You should also be able to place a code block inside a list or a quote without breaking it, and to diagnose the classic “the whole page turned into a code block” failure by counting fences. In the next page, inline code, you meet the smaller unit of the same idea: a handful of characters that stay literal in the middle of a sentence.

Found an issue with this content?

Tell us what could be improved.