import jsonfrom pathlib import Path
def load(path: Path) -> dict: with path.open() as fh: return json.load(fh)
cfg = load(Path("config.toml"))cfg = load(Path("config.json"))print(cfg["name"])
starlight-codeblocks
Features
Section titled “Features”- Annotations
- Side annotations
- Footnotes
- Inline callouts
- Scrollycoding
- Code walkthrough
- Focus
- Line states
- Squiggles
- Code mentions
- Hidden lines
- Expandable blocks
- Visible whitespace
- Colourised brackets
- Colour swatches
- File icons
- Inline code highlighting
- Word-level diff
- Code links
- API auto-linking
- Line permalinks
- Code tabs
- Fill-in placeholders
- Smart shell copy
- Open in playground
- Run code
Annotations
Add numbered markers to lines. Each marker opens a note in a popover, so the code stays clean until a reader asks.
on: [push, pull_request]jobs: test: runs-on: ubuntu-latest1Uses the Ubuntu GitHub Actions runner.
steps: - uses: actions/checkout@v7 - uses: astral-sh/setup-uv@v72Installs uv and caches its downloads between runs.
- run: uv run pytest- Uses the Ubuntu GitHub Actions runner.
- Installs
uvand caches its downloads between runs.
Side annotations
Show the notes of an annotated block in a column beside the code, so readers see every note next to its line.
import csvimport sysfrom collections import Counterfrom pathlib import Path
def read_rows(path): with path.open() as fh:1 return list(csv.DictReader(fh))2
def summarise(rows, key):3 return Counter(r[key] for r in rows)
def main(): rows = read_rows(Path(sys.argv[1])) counts = summarise(rows, "status") for status, n in counts.items(): print(f"{status:<10} {n:>5}")4
if __name__ == "__main__": main()- 1Note 1, for line 8: Opens the file and closes it when the block ends.
- 2Note 2, for line 9: Each row becomes a dict keyed by the header line.
- 3Note 3, for line 12: Counts the rows for each value in the column
key. - 4Note 4, for line 20: Pads the status and count into columns of fixed width.
Footnotes
Add numbered badges to lines, with the notes in a list under the block, where readers see them all at once.
Inline callouts
Put a short note in a bubble above a line, with an arrow that points at the word it explains.
const controller = new AbortController();Lets controller.abort() cancel the request.const res = await fetch(url, { signal: controller.signal });const data = await res.json();Scrollycoding
Explain a code block in prose steps that scroll past it, while the block stays in view and focuses the lines of each step.
import express from 'express';
const app = express();app.use(express.json());
app.get('/health', (req, res) => { res.json({ ok: true });});
app.listen(3000);import express from 'express';
const app = express();app.use(express.json());
app.get('/health', (req, res) => { res.json({ ok: true });});
app.listen(3000);import express from 'express';
const app = express();app.use(express.json());
app.get('/health', (req, res) => { res.json({ ok: true });});
app.listen(3000);import express from 'express';
const app = express();app.use(express.json());
app.get('/health', (req, res) => { res.json({ ok: true });});
app.listen(3000);import express from 'express';
const app = express();app.use(express.json());
app.get('/health', (req, res) => { res.json({ ok: true });});
app.listen(3000);import express from 'express';
const app = express();app.use(express.json());
app.get('/health', (req, res) => { res.json({ ok: true });});
app.listen(3000);Code walkthrough
Step through versions of one code block, and watch the code move from each version to the next, so readers see what changed.
const app = express();
app.listen(3000);const app = express();app.use(express.json());
app.listen(3000);const app = express();app.use(express.json());
app.get('/health', (req, res) => { res.json({ ok: true });});
app.listen(3000);Focus
Blur the lines outside a range, so that readers look at the lines that you name first.
import { defineConfig } from './lib.js';
export default defineConfig({ cache: { dir: '.cache', maxAge: 3600, }, retries: 2, verbose: false,});Line states
Tint lines as errors, warnings, notes or successes, with an optional message after the code, like the diagnostics in a code editor.
Note:import sysNote
Error:for name in sys.argv[1:]Error SyntaxError: expected ':' print(name)
Warning:count = len(sys.argv)Warning Includes the script nameSquiggles
Draw a wavy error, warning or info line under code, as a code editor does, with a message that shows on hover.
import { readFile } from 'node:fs/promises';import { join (Warning: join is declared but its value is never read.)Warningjoin is declared but its value is never read. } from 'node:path';
const config = JSON.parse(await readFle (Error: Cannot find name readFle. Did you mean readFile?)ErrorCannot find name readFle. Did you mean readFile?('config.json', 'utf8'));- Line 2, warning:
joinis declared but its value is never read. - Line 4, error: Cannot find name
readFle. Did you meanreadFile?
Code mentions
Link a phrase in the prose to lines of the code block below it, so that readers see which lines the text is about.
The base case stops the recursion. The recursive step calls the function again with a smaller number.
def factorial(n): if n == 0: return 1 return n * factorial(n - 1)Hidden lines
Hide the imports and set-up that readers need to run an example but not to understand it.
import jsonfrom 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}")Expandable blocks
Show the first lines of a long block, with a fade and a button to reveal the rest.
import csvimport sysfrom collections import Counterfrom pathlib import Path
def read_rows(path: Path) -> list[dict]: with path.open() as fh: return list(csv.DictReader(fh))
def summarise(rows: list[dict]) -> Counter: return Counter(row["status"] for row in rows)
def main() -> None: rows = read_rows(Path(sys.argv[1])) counts = summarise(rows) for status, n in counts.most_common(): print(f"{status:<10} {n:>5}")
if __name__ == "__main__": main()Visible whitespace
Show spaces and tabs as faint glyphs, for the blocks where indentation changes the meaning of the code.
build: cargo build --release
test: cargo test # Recipes must start with a tabColourised brackets
Colour matching brackets by nesting depth, so a dense line of code stays readable.
const title = document.title;const slug = encodeURIComponent( kebab(trim(title.toLowerCase())));Colour swatches
Show a small swatch of each CSS colour next to its value, so readers see the colour without a colour picker.
.button { color: #ffffff; background: rebeccapurple; border: 1px solid rgb(102 51 153 / 50%);}File icons
Show the icon of the file type before the title of a code block, with the icons of the Starlight file tree or a coloured icon set.
The default vscode-icons:
export const greet = (name) => console.log('Hello', name);Material Icon Theme, with fileIcons.set="material":
print("Hello from Python")The Seti icons of the Starlight file tree, with fileIcons.set="seti":
{ "name": "my-site", "type": "module" }Catppuccin, with fileIcons.set="catppuccin":
FROM node:22-alpineCMD ["node", "src/index.js"]Inline code highlighting
Give inline code in the prose the syntax colours of the code blocks, from a language suffix or a default language for the site.
- In JavaScript,
[] + {}is"[object Object]".- In Python,
from __future__ import bracesraisesSyntaxError: not a chance.- In CSS,
.modal { z-index: 99999 !important }is a cry for help.- In a shell,
rm -rf node_modulesfixes most things.
Word-level diff
Highlight the words that changed inside each line of a diff, so readers find a small edit in a long line.
const timeout = 5000;const timeout = options.timeout ?? 5000;const client = createClient({ retries: 3, retries: options.retries,});Code links
Turn text in code into a link with a card that describes it, from a directive in the comment above.
import numpy as np
y = np.sin(2 * np.pi * x)API auto-linking
Link the names in code examples to their reference pages, with a card that shows the signature and a summary.
Line permalinks
Give a code block line numbers that link to each line, so readers can share a link to the exact lines they mean. Shift-click to select multiple lines.
Code tabs
Show several code blocks as one, with editor tabs in the title bar, such as the files of a project or the commands for each package manager.
name: CIon: pushjobs: greet: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - run: python greet.py - run: node greet.jsimport sys
name = sys.argv[1] if len(sys.argv) > 1 else "world"print(f"Hello, {name}!")const name = process.argv[2] ?? 'world';console.log(`Hello, ${name}!`);Fill-in placeholders
Turn placeholders such as YOUR_TOKEN into fields, so readers type their own values into every block and the copied code.
curl -H "Authorization: Bearer YOUR_TOKEN" \ https://api.example.com/workspaces/WORKSPACE_ID/runsfrom example import Client
client = Client(token="YOUR_TOKEN")Smart shell copy
Add a Copy commands button to terminal blocks, which copies the commands without the prompts or the output.
$ uv tool install ruffResolved 1 package in 180msInstalled 1 executable: ruff$ ruff check src/ \ --fixFound 3 errors (3 fixed, 0 remaining).Open in playground
Add a title bar button that opens the example in an online playground, with the code already filled in.
type Status = 'queued' | 'running' | 'done';
function label(s: Status): string { return s === 'done' ? 'Finished' : s;}
console.log(label('done'));Run code
Add a Run code button that runs the example in the browser and shows the output under the block.
from statistics import mean, stdev
reads = [1520, 1610, 1480, 1575]print(f"mean {mean(reads):.0f}, sd {stdev(reads):.1f}")Using with an agent
Section titled “Using with an agent”This website is agent-friendly: each page has a Markdown version at the same address with .md at the end, such as install/starlight.md.
llms.txt lists every page, and llms-full.txt has every page in one file.
Using an agent? Point it at the bundled skill. The skill tells the agent which feature suits each use, and how to write it.
Thanks
Section titled “Thanks”This plugin builds on the excellent Expressive Code, which renders every code block in Starlight.
The [!code …] comment notation comes from the Shiki transformers and VitePress. Thank you to both projects. Most code blocks from VitePress work here unchanged.