Skip to content

Scrollycoding

A walkthrough of a file often puts the whole block first, then explains it in paragraphs below. Readers scroll back up to find each line that a paragraph names. Scrollycoding keeps the block next to the prose. As each step scrolls past the middle of the block, the block focuses the lines of that step.

Readers see

Import Express. Nothing else is needed for a small server.
server.js
import express from 'express';
const app = express();
app.use(express.json());
app.get('/health', (req, res) => {
res.json({ ok: true });
});
app.listen(3000);
Create the app object that holds routes and middleware.
server.js
import express from 'express';
const app = express();
app.use(express.json());
app.get('/health', (req, res) => {
res.json({ ok: true });
});
app.listen(3000);
Parse JSON request bodies before any route sees them.
server.js
import express from 'express';
const app = express();
app.use(express.json());
app.get('/health', (req, res) => {
res.json({ ok: true });
});
app.listen(3000);
Answer health checks with a small JSON payload.
server.js
import express from 'express';
const app = express();
app.use(express.json());
app.get('/health', (req, res) => {
res.json({ ok: true });
});
app.listen(3000);
Start listening on port 3000.
server.js
import express from 'express';
const app = express();
app.use(express.json());
app.get('/health', (req, res) => {
res.json({ ok: true });
});
app.listen(3000);
server.js
import express from 'express';
const app = express();
app.use(express.json());
app.get('/health', (req, res) => {
res.json({ ok: true });
});
app.listen(3000);

You write

<Scrollycoding>
```js title="server.js"
import express from 'express';
const app = express();
app.use(express.json());
app.get('/health', (req, res) => {
res.json({ ok: true });
});
app.listen(3000);
```
<Step focus="1">Import Express. Nothing else is needed for a small server.</Step>
<Step focus="3">Create the app object that holds routes and middleware.</Step>
<Step focus="4">Parse JSON request bodies before any route sees them.</Step>
<Step focus="6-8">Answer health checks with a small JSON payload.</Step>
<Step focus="10">Start listening on port 3000.</Step>
</Scrollycoding>

Scroll down the page. The block stays below the site header, and each step that reaches the middle of the block changes the focus. On a narrow screen, each step has its own copy of the block, focused for that step.

Syntax Where
<Scrollycoding> … </Scrollycoding> Around the code blocks and the steps, in an MDX file
<Step> … </Step> After the code block, one for each step
focus="<range>" On <Step>
mark="<range>" On <Step>
A code block between two steps After a <Step>
codeSide="left" On <Scrollycoding>

Import both components at the top of the MDX file:

import { Scrollycoding, Step } from 'starlight-codeblocks/components';

Put one code block first, then the steps. Leave an empty line after <Scrollycoding> and before </Scrollycoding>. focus and mark take a range without braces, such as 4 or 1, 6-8. A step can contain any Markdown, such as links, inline code or several paragraphs. Text outside the steps fails the build: put it in a step, or before or after <Scrollycoding>.

To change the code during the walkthrough, put a new version of the code block between two steps. The steps after it show the new version, and their focus and mark ranges count its lines. See Change the code between steps.

A step can mark a line as well as focus a range. Here, the second step keeps the whole function in focus and marks the line that changes the total:

The function takes the items and an optional discount, from 0 to 1.
cart.py
def total(items, discount=0):
price = sum(i.price for i in items)
return price * (1 - discount)
The last line applies the discount to the sum.
cart.py
def total(items, discount=0):
price = sum(i.price for i in items)
return price * (1 - discount)
cart.py
def total(items, discount=0):
price = sum(i.price for i in items)
return price * (1 - discount)

A code block between two steps is a new version of the code. Here, the third step adds a line, and the block animates to the new version when that step becomes active:

Create the app object that holds routes and middleware.

Code from this step on:

const app = express();

app.listen(3000);
server.js
const app = express();
app.listen(3000);
Start listening on port 3000.
server.js
const app = express();
app.listen(3000);
Parse JSON request bodies before any route sees them.

Code from this step on:

const app = express();
app.use(express.json());

app.listen(3000);
server.js
const app = express();
app.use(express.json());
app.listen(3000);
The server still listens on port 3000.
server.js
const app = express();
app.use(express.json());
app.listen(3000);
server.js
const app = express();
app.listen(3000);
server.js
const app = express();
app.use(express.json());
app.listen(3000);

codeSide="left" puts the block in the left column and the steps in the right column:

The function takes the items and an optional discount, from 0 to 1.
cart.py
def total(items, discount=0):
price = sum(i.price for i in items)
return price * (1 - discount)
Add up the price of each item.
cart.py
def total(items, discount=0):
price = sum(i.price for i in items)
return price * (1 - discount)
Apply the discount to the sum.
cart.py
def total(items, discount=0):
price = sum(i.price for i in items)
return price * (1 - discount)
cart.py
def total(items, discount=0):
price = sum(i.price for i in items)
return price * (1 - discount)
  • Layout:
    • On a wide screen, the steps are a column on the left, and the block is on the right. With codeSide="left", the block is on the left and the steps are on the right. The block stays below the site header while the steps scroll with the page.
    • The two columns need space for the longest line of the block beside a text column of 12rem. The plugin measures the lines at build time and gives the block one of three widths: 600, 800 or 1000 px. In a narrower container, the page shows the narrow layout. The widths are the same as for side annotations.
    • On a narrow screen, each step shows its own copy of the block below its text, in the version of that step.
  • Scrolling through the steps:
    • The step that crosses the middle of the block is the active step. It shows at full opacity, and the other steps fade.
    • When the active step changes, the block changes its focused and marked lines, with the 250 ms transition of focus.
    • When the active step has a new version of the code, the code moves to the new version as in a code walkthrough. Code that is in both versions moves, and new lines fade in with a green tint.
    • The block stays blurred when the mouse cursor is over it, because the mouse cursor often rests there while the reader scrolls. Move keyboard focus into the block to make every line sharp.
  • Copy:
    • The copy button copies the whole block, in every step.
  • Without JavaScript:
    • The page shows the narrow layout at every width.
  • Motion:
    • When the reader’s system asks for reduced motion, the focus, the fade and the code change without a transition.

Scrollycoding has no options. To turn the feature off for the whole site, set scrollycoding to false:

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

With scrollycoding: false, <Scrollycoding> shows the narrow layout at every width, with no script. With walkthrough: false, a new version of the code shows without the animation.

A block with lines of more than about 41 characters needs more than 600 px for the two columns. On a page without a table of contents, the content column has free space on each side. A <Scrollycoding> that needs 800 or 1000 px spreads over that space, as side annotations do.

See the demo on a page without a table of contents

To use this, turn off the table of contents of the page with tableOfContents: false in its front matter. The component spreads only when it is directly on the page, not in tabs, an aside or another component.

  • <Scrollycoding> works in MDX files only, because Markdown files cannot contain components.
  • During the animation to a new version, the block shows the syntax colours only. The focus shows again when the animation ends.
  • The widths are estimates for Starlight’s default code font. A larger code font can make a line scroll in the code column.
  • The narrow layout repeats the block for each step, so a long block with many steps makes a long page.
  • The component replaces the focus of the block. A focus attribute on the fence line (the first line of the code block, with the language) has no effect. A mark on the fence line, such as {2}, shows in every step. The steps focus their lines with focus: false too.
  • Focus: one block with one focus range, with no prose steps.
  • Code walkthrough: versions of a block, with buttons to step through them and no prose between the steps.
  • Side annotations: notes for many lines in a column next to the block, all visible at once.