Skip to content

Hidden lines

A working example often needs lines that the reader can skip, such as imports, settings or the boilerplate around the part you explain. The hidden attribute and [!code hide] remove these lines from view, behind a marker the reader can open.

Readers see

summary.py
import json
from pathlib import Path
config = json.loads(Path("config.json").read_text())
for key, value in config.items():
if key.startswith("_"):
continue
print(f"{key} = {value}")

You write

```py title="summary.py" hidden={1-3,6-7}
import json
from pathlib import Path
config = json.loads(Path("config.json").read_text())
for key, value in config.items():
if key.startswith("_"):
continue
print(f"{key} = {value}")
```

The imports and the if block are behind their own markers. Click a marker to show that run, or the title bar button to show every hidden line at once. Copying the block gives the whole file, imports included.

Syntax Where
hidden={range} Code block fence line
[!code hide] Comment
[!code hide:N] Comment

hidden={range} takes a range, such as {2} or {1, 4-6}. [!code hide:N] hides its own line and the next N-1 lines. You can use the attribute and the directive in the same block.

A directive keeps the hidden line next to the code it belongs to, so it moves if you reorder the block:

server.ts
import { createServer } from 'node:http';
import { readFileSync } from 'node:fs';
const port = Number(process.env.PORT ?? 3000);
createServer((req, res) => res.end(readFileSync('index.html'))).listen(port);

A block with no title still gets a title bar, which holds only the toggle button:

import { readFile } from 'node:fs/promises';
const notes = await readFile('notes.txt', 'utf8');
console.log(notes.length);
  • Markers:
    • Each run of hidden lines is replaced by a dashed marker, with the text “N hidden lines”.
    • Clicking a marker shows that run only. Its text changes to “Hide N lines”. The lines get a faint background, and their code is dimmed to 75% opacity, so the reader can tell they asked for them.
    • The dashed rule of a marker gets brighter when the mouse cursor is on it, and fainter while its lines show.
  • The title bar button:
    • The title bar has a button, “Show N hidden lines”, where N is the total across every run. It shows every run at once. Once every run is open, it reads “Hide N lines”.
  • Screen readers:
    • Each marker has aria-expanded. The label of the title bar button changes from Show to Hide. Both have aria-controls, so assistive technology reports what they open.
  • Copy and print:
    • The copy button always includes hidden lines, so the code still runs after a paste. A manual selection includes only the hidden lines that are open, and never the text of a marker.
    • Hidden lines stay hidden when the page prints. The markers and the title bar button do not print.
  • Other features:
    • An inline callout on a hidden line shows only while the line is open.
  • Without JavaScript:
    • Hidden lines stay hidden, and the markers and the title bar button do nothing.

Hidden lines have no options. Turn the feature off for the whole site with hiddenLines: false:

astro.config.mjs
codeblocks({
hiddenLines: false,
});

With hiddenLines: false, the plugin ignores hidden={range}, and [!code hide] stays in the code as written.

Expressive Code has its own plugin for a similar job, @expressive-code/plugin-collapsible-sections. Its collapse={range} attribute folds lines into a section with a summary line, such as “3 collapsed lines”. The plugin is not part of Starlight, so a site must install it and add it to the plugins list of its ec.config.mjs file. That list replaces the plugins of Starlight, so add pluginCodeblocks() to it too, as in Sites with an ec.config.mjs file:

ec.config.mjs
import { pluginCollapsibleSections } from '@expressive-code/plugin-collapsible-sections';
import { pluginCodeblocks } from 'starlight-codeblocks/expressive-code';
export default {
plugins: [pluginCollapsibleSections(), pluginCodeblocks()],
};

This site uses the plugin, so the examples below are live. Here is the first example of this page again, with collapse in place of hidden:

Readers see

summary.py
3 collapsed lines
import json
from pathlib import Path
config = json.loads(Path("config.json").read_text())
for key, value in config.items():
2 collapsed lines
if key.startswith("_"):
continue
print(f"{key} = {value}")

You write

```py title="summary.py" collapse={1-3,6-7}
import json
from pathlib import Path
config = json.loads(Path("config.json").read_text())
for key, value in config.items():
if key.startswith("_"):
continue
print(f"{key} = {value}")
```

Each section keeps a summary line in the block, where hidden lines leave only a thin marker. Click a summary line to open its section.

With collapseStyle, an open section keeps a line to close it again. collapsible-auto puts that line at the start of the section, or at the end if the section ends the block:

Readers see

summary.py
import json
from pathlib import Path
config = json.loads(Path("config.json").read_text())
for key, value in config.items():
2 collapsed lines
if key.startswith("_"):
continue
print(f"{key} = {value}")

You write

```py title="summary.py" collapse={6-7} collapseStyle="collapsible-auto"
import json
from pathlib import Path
config = json.loads(Path("config.json").read_text())
for key, value in config.items():
if key.startswith("_"):
continue
print(f"{key} = {value}")
```

The two features compare as follows:

Aspect Hidden lines Collapsible sections
Space before a reader opens it A thin dashed marker A full summary line for each section
Syntax hidden={range}, [!code hide] and [!code hide:N] collapse={range}
Show every section at once A title bar button No
Open lines look different Yes: a faint background, and code at 75% opacity Only with a collapsible-* style
Close a section again The marker, or the title bar button The summary line, with a collapsible-* style
Copy button Copies every line Copies every line
Without JavaScript The lines stay hidden Works: the sections are HTML <details> elements
Needs This plugin A separate Expressive Code plugin

Use hidden lines for lines that most readers never need, such as imports, and when the block must stay compact. The directive keeps the hidden state next to its line, so it moves when you edit the block. Use collapsible sections when readers often open the lines, or when the page must work without JavaScript. Both plugins work on one site, and each block can use either one.

  • A hidden line still runs in the copied code. If a line depends on something the reader must change, such as an API key, keep it visible or use a fill-in placeholder instead.
  • Focus: blur the lines around the ones you explain, and keep them in view.
  • Expandable blocks: cap a long block at a line count, instead of a list of lines to hide.
  • Collapsible sections: the Expressive Code plugin that folds lines behind a summary line.
  • Comment notation: the directive syntax that [!code hide] uses.