Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Open In Colab Open In Kaggle

This page is itself a scrollytelling page. Each card explains one feature and shows its syntax, and the stage next to it shows that feature working. W2D1 Tutorial 2 is a full tutorial built this way.

neuromatch course kit

Tutorial 7: Scrollytelling pages

A pinned stage of figures, videos and live Python, with text cards that scroll past.

Time: about 20 minutes

A scrolly page

  1. Is still one flat MyST .md file

  2. Still produces a normal notebook for Colab and Kaggle

  3. Shows real figures before Python starts

  4. Runs Python in the browser when the reader asks

Step options

  1. hero: full screen, no card

  2. :section: label in the progress bar

  3. :kicker: label above the card

  4. {stage} for stage-only markdown

# The demo from the last step, as it appears in the notebook
# @title Sliders and dropdowns
import ipywidgets as widgets


@widgets.interact(freq=widgets.FloatSlider(value=2.0, min=0.5, max=5, step=0.5, continuous_update=False), shape=widgets.Dropdown(options=['sine', 'square', 'sawtooth'], value='sine'))
def _demo(freq, shape):
    t = np.linspace(0, 2, 1000)
    ...

Video 1: Linear Dynamical Systems

YouTube
Bilibili

Video available at https://youtube.com/watch?v=87z6OR7-DBI

Video available at https://www.bilibili.com/video/BV1up4y1S7wj

# myst.yml
extend: https://cdn.jsdelivr.net/npm/@neuromatch/course-kit@0.1/dist/nma.yml  # the course kit's MyST plugin

nma:
  scrolly:
    pyodide_url: https://cdn.jsdelivr.net/pyodide/v314.0.7/full/
# Preview with real outputs locally
uv run nma-kit render-scrolly
uv run myst start

# Writes _build/scrolly/<Day>/<Tutorial>.json

that's it

Now write your own

Start from W2D1 Tutorial 2, or add layout: scrolly and a few {step} markers to a page you already have.

What and when

What a scrolly page is

A scrolly page is an ordinary tutorial .md file with one extra frontmatter line. The page is split into steps. Each step has a card (the text you are reading) and a stage (the pinned area next to it, or above it on a phone).

Use it for narrative walkthroughs: a sequence of figures, short demos and videos, where each idea needs one picture.

Don’t use it for long reference material, or for pages that are mostly heavy exercises. Those read better as normal pages.

Turning it on

layout: scrolly and {step}

Add layout: scrolly to the frontmatter, hide the “On this page” outline (recommended, see below), then put a {step} marker wherever a new step starts. A step runs from its marker to the next one. Markers have no body: the content goes after the closing fence.

---
title: "Tutorial 2: My topic"
layout: scrolly
site:
  hide_outline: true
kernelspec:
  name: python3
  display_name: Python 3
---

:::{step} hero
:::
# My topic

:::{step}
:::
Text for the first card.

:::{step}
:::
Text for the second card.

MyST logs extra key ignored: layout, which is expected.

hide_outline removes the “On this page” outline on this page only, which gives the scrolly section more room. Keep the table of contents (don’t set hide_toc): on a page this long it is how readers move to other pages without scrolling to the end. The section fits between the menus, and when that is too narrow the stage sits above the cards instead of beside them. The previous/next links at the bottom of the page work as on any other page.

Options

hero, :section:, :kicker: and {stage}

  • {step} hero makes a full-screen title or closing step, with no card. The first step of this page is one.

  • :section: sets the label in the progress bar at the top of the stage. It carries forward until the next step that sets it.

  • :kicker: is the small label above a card, like “Options” above this one.

  • {stage} holds markdown that only appears on the stage. :style: list turns a list into the numbered chips shown here. :class: adds CSS classes.

:::{step}
:section: Basics
:kicker: Options
:::
Card text.

:::{stage}
:style: list
1. Shown on the stage as a chip
2. Another chip
:::

Stage or card

What goes where

The plugin sorts each step’s content for you:

StageCard
code cells (except remove-cell)prose, math, headings
images and {figure} (also .mp4){nma-solution} / {nma-explanation} links
{nma-video}, {nma-slides}{nma-feedback}
the {stage} blockeverything else

If a step has nothing for the stage, the previous stage stays. This step is an example: the chips from the last step are still showing. Content before the first {step} renders as a normal page header.

Code cells

An editable cell

An untagged code cell becomes an editable panel with Run and Reset. Change the code and run it (Ctrl+Enter also works).

The first Run downloads Python into your browser. All cells on the page share one namespace, and running a cell first runs any earlier cells that haven’t run yet. So rng below comes from the hidden setup cell at the top of the page, without you running it.

Code cells

Tags choose how a cell looks

TagOn the stage
noneeditable panel (code + output)
hide-inputcode collapsed behind “Show code”
remove-inputoutput only
remove-cellinvisible, runs before the first visible cell

This step’s cell is tagged hide-input: the output shows, and the code is one click away.

Code cells

Output-only figures

A remove-input cell shows only its output. Use it for figures that tell the story, where the code would be a distraction.

```{code-cell} python
:tags: [remove-input]
plt.plot(...)
plt.show()
```

Interactive demos

Sliders and dropdowns with # @param

Mark an assignment with a Colab form comment, and the panel gets a control. Moving it reruns the cell.

freq = 2.0      # @param {type:"slider", min:0.5, max:5, step:0.5}
shape = "sine"  # @param ["sine", "square", "sawtooth"]

Rules: one top-level assignment per line, numbers for sliders (step defaults to 1), and a list of literals for dropdowns that includes the default. Before Python starts, the stage shows the output for the defaults.

Interactive demos

From widgets.interact to # @param

Existing NMA demos use ipywidgets. On a scrolly page, rewrite them with # @param:

# Before
@widgets.interact(a=widgets.FloatSlider(value=-0.5, min=-2.5, max=1.5, step=0.25))
def plot_demo(a):
  plt.plot(t, np.exp(a * t)); plt.show()

# After
a = -0.5  # @param {type:"slider", min:-2.5, max:1.5, step:0.25}
plt.plot(t, np.exp(a * t)); plt.show()

You don’t lose anything in the notebooks: the generated .ipynb turns every # @param cell back into a widgets.interact demo, as shown on the stage.

Exercises

Exercises, solutions and explanations

These work exactly as in Tutorial 5: an untagged exercise cell, directly followed by an {nma-solution}. The card gets the Click for solution link.

Try it: fill in cube, or don’t. If you run the next cell first, the page runs the reference solution in place of the unfinished exercise and says so, so you can always run ahead.

Exercises

Running ahead

This cell uses cube from the exercise. Run it before finishing the exercise to see the fallback note.

Media and feedback

Videos and feedback

{nma-video} and {nma-slides} go on the stage, and {nma-feedback} stays on the card. Both work as in Tutorial 4. Media keep playing only while their step is active: muted .mp4 figures play and pause as you scroll.

Did this step make sense?

Browser Python

Python in the browser

The Enable Python pill at the top of the stage (or any Run button) starts Pyodide once for the page. The first load is about 10 MB plus the packages a cell imports, and the browser then caches it. JupyterLite is not used on scrolly pages, so its power button is hidden.

  • Packages come from the cells’ imports. numpy, matplotlib, scipy and pandas all work. Anything Pyodide can’t run belongs in the Colab notebook.

  • No sockets: load data files with pyodide.http.open_url under sys.platform == "emscripten" (see the setup cell of W2D1 Tutorial 2).

  • The Pyodide build comes from nma.scrolly.pyodide_url in myst.yml. The host must send CORS headers.

Pre-rendered outputs

Real figures before Python starts

In CI, nma-kit render-scrolly runs every scrolly page’s instructor notebook (solutions filled in, # @param at their defaults) and saves the outputs. The plugin puts them in the page, so every figure above appeared without starting Python. Never type outputs by hand.

  • A cell that raises fails the build, naming the file and line.

  • Exercise cells get no starting output.

  • Without a render file (for example, plain myst start), panels show Run to see output.

Notebooks

The Colab and Kaggle version

A scrolly page with a kernelspec still gets a linear notebook, generated the usual way:

  • {step} markers disappear. A :kicker: becomes an italic line.

  • {stage} blocks become plain markdown.

  • # @param cells become widgets.interact demos.

  • Videos, solutions and feedback become the usual NMA cells, with student and instructor versions.

Open this page’s notebook from the badges at the top to see the result. A scrolly page without code or kernelspec is site-only.

Converting a tutorial

Converting an existing tutorial

W2D1 Tutorial 2 is Tutorial 1 converted this way:

  1. Copy the page and add layout: scrolly, plus site: hide_outline: true.

  2. Merge setup, figure style and helpers into one remove-cell cell. Drop % magics and the ipywidgets import.

  3. Add a {step} before each idea that needs its own picture, with :section: at section starts and :kicker: labels.

  4. Rewrite widgets.interact demos with # @param.

  5. Add remove-input figures where a card has nothing to show.

  6. Avoid “on the right” in card text: the notebook reads it too.

  7. Run --render and myst start, and scroll through it.

CI errors

Errors you may see

MessageFix
{step} only works on pages with layout: scrollyAdd layout: scrolly, or remove the marker
page has layout: scrolly but no {step} markersAdd at least one {step}
{step} has no bodyMove the content below the closing :::
{step} takes no argument or heroUse {step} or {step} hero
unknown option{step}: :section:, :kicker:. {stage}: :style:, :class:
step has more than one {stage} / before the first {step}One {stage} per step, after its marker
must be at the top levelDon’t nest markers in other directives
# @param must not be indented / must be an assignmentOne top-level name = value # @param ... per line
default ... is not one of the optionsInclude the default in the dropdown list
T.md:42: ZeroDivisionError (render)Fix the cell at that line; it raised in CI