Writing a story file
A story file is one YAML document, type: scrollytelling, that describes a
story map: a map, and the chapters that scroll over it. Each chapter moves the
camera, and can show or hide layers and run actions (this is often called
scrollytelling). This page explains how to write one. To put it on a page,
see Story maps in plain HTML or
Story maps in Astro; every key, with its type
and default, is in the generated
YAML reference for stories.
Quick start
Section titled “Quick start”A story needs a type, an id, the map’s config: and at least one chapter.
This one draws the past week’s earthquakes from the USGS feed and visits two
places:
type: scrollytellingid: quakes-this-week
# The map when the page opens: its basemap and camera.config: mapStyle: "https://tiles.openfreemap.org/styles/liberty" center: [0, 20] zoom: 1.5
# Layers drawn for the whole story.layers: - id: quakes type: circle source: type: geojson url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_week.geojson" paint: circle-radius: 5 circle-color: "#dc2626"
chapters: - id: world title: "A week of earthquakes" description: "<p>Every earthquake of magnitude 4.5 or more in the past seven days.</p>" center: [0, 20] zoom: 1.5 - id: japan title: "Japan" description: "<p>Four plates meet under Japan.</p>" center: [138, 37] zoom: 4.5Show it with <ml-scrollytelling src="story.yaml"> or the Astro
<Scrollytelling> component. The map opens on the whole world; scroll and it
flies to Japan as the second card reaches the middle of the screen.
idnames the story. Keep it unique on the page.config:is the map’s starting point, as in any map document:mapStyle(the basemap),centerandzoom. Its other keys (maxZoom,locale, gestures) work too.layers:are drawn for the whole story. Chapters show, hide and restyle them.- Each chapter needs an
id, atitle, acenterand azoom.
Check a story without a browser with npx maplibre-yaml@latest validate story.yaml.
Each problem is listed with its line number and, for a misspelt key, a
did-you-mean (centre → center). In the browser, the same list shows in
place of the story.
Common tasks
Section titled “Common tasks”Write a chapter’s text, pictures and video
Section titled “Write a chapter’s text, pictures and video”descriptionis HTML, shown as written:<p>,<strong>,<a href="…">. Markdown is not converted, so**bold**shows literally.image:andvideo:show above the text. Give a fullhttps://URL, or the path of a file on your site: on an Astro or Vite site, a file inpublic/images/fault.jpgis/images/fault.jpg. Images load only as the reader reaches them, and are never taller than 40% of the screen.
chapters: - id: fuji title: "Mount Fuji" description: "<p>Last erupted in <strong>1707</strong>.</p>" image: "/images/fuji.jpg" center: [138.73, 35.36] zoom: 9A chapter with hidden: true has no card: the map moves, and the reader sees
nothing but the map until the next chapter.
Place the card
Section titled “Place the card”alignment: puts the card on the left, on the right, in the center
(the default), or across the full width. The camera is offset so the
chapter’s center isn’t hidden behind its card: beside a left or right card,
above a centred or full-width one. On phones every card sits in the lower part
of the screen, and the chapter’s center shows above it.
chapters: - id: intro title: "Introduction" center: [0, 20] zoom: 1.5 alignment: full - id: andes title: "The Andes" center: [-70, -20] zoom: 4 alignment: leftcameraOffset: false, at the top of the file, turns the offset off: each
center is then the middle of the whole map. A chapter’s own padding: (in
pixels, one number or { top, right, bottom, left }) replaces the offset for
that chapter.
Move the camera
Section titled “Move the camera”Each chapter sets the camera with center and zoom, and optionally pitch
(tilt, 0–85) and bearing (rotation, −180 to 180). animation: says how the
map gets there:
flyTo(the default) zooms out, travels and zooms back in, in an arc.speed(default 0.6) andcurve(default 1) tune the arc.easeTomoves straight there, with no zoom-out arc. Use it between nearby places.jumpTocuts there at once.
chapters: - id: tokyo title: "Tokyo from above" center: [139.69, 35.69] zoom: 12 pitch: 60 bearing: -30 animation: easeToWhile a chapter is active, rotateAnimation: true slowly turns the camera,
and spinGlobe: true slowly pans it westward (for a globe, set
projection: { type: globe } at the top of the file). Both stop at the next
chapter. When the reader’s system asks for reduced motion, every camera move
is instant and neither runs.
Show and hide layers
Section titled “Show and hide layers”layers: { show: [...], hide: [...] } on a chapter changes which layers are
visible, by layer id. The names can also be the basemap’s own layers.
chapters: - id: plates title: "Plate boundaries" center: [140, 20] zoom: 3 layers: show: [plate-boundaries] hide: [quakes]A chapter’s changes stay until another chapter undoes them. Give each layer
its starting state in the layer itself: a layer that a later chapter reveals
starts with layout: { visibility: none }. A name that is a near miss of one
of the story’s layers (quake for quakes) gets a did-you-mean warning; a
name unlike any of them is taken to be a basemap layer.
Run actions as a chapter starts or ends
Section titled “Run actions as a chapter starts or ends”onChapterEnter runs when a chapter becomes active, after its camera move and
layer changes. onChapterExit runs when the reader scrolls past it, before
the next chapter’s camera move. Each is a list of actions:
action | What it does | Its fields |
|---|---|---|
setFilter | Filter a layer’s features | layer, filter (an expression, or null to clear it) |
setPaintProperty | Change a paint property | layer, property, value |
setLayoutProperty | Change a layout property | layer, property, value |
flyTo | Fly the camera | options: center, zoom, pitch, bearing, duration, … |
easeTo | Ease the camera, with no arc | options, as for flyTo |
fitBounds | Frame an area | bounds: [west, south, east, north] or [[west, south], [east, north]]; options: padding, duration, … |
custom | Run nothing itself; tells your page script | options: anything your script reads |
chapters: - id: strongest title: "The strongest" center: [0, 20] zoom: 1.5 onChapterEnter: - action: setFilter layer: quakes filter: [">=", ["get", "mag"], 6] - action: setPaintProperty layer: quakes property: circle-radius value: 10 onChapterExit: - action: setFilter layer: quakes filter: null - action: setPaintProperty layer: quakes property: circle-radius value: 5An action that names a layer the map doesn’t have does nothing, and the map’s
notice says so while you develop. Under reduced motion the camera actions are
instant, unless their options set essential: true.
Run your own code from a chapter
Section titled “Run your own code from a chapter”A custom action runs nothing itself. The story element dispatches an
ml-scrollytelling:action event with the action, so a script on the page can
do anything with it:
chapters: - id: deep title: "Deep earthquakes" center: [-70, -20] zoom: 4 callback: deep-quakes onChapterEnter: - action: custom options: { banner: "Deeper than 300 km" }const banner = document.getElementById("banner");document.addEventListener("ml-scrollytelling:action", (event) => { const { action, phase } = event.detail; // also: chapterId, and map (the MapLibre map) if (phase === "enter") banner.textContent = action.options.banner;});callback: is a name you choose. It does nothing by itself: it arrives as
callback on the ml-scrollytelling:chapter event, which fires on every
chapter change. Both events are in the
<ml-scrollytelling> reference.
Add the chapter rail
Section titled “Add the chapter rail”showMarkers: true adds a column of dots at the right edge of the map, one
per chapter. Despite the key’s name, nothing is drawn on the map itself; to
mark chapter places on the map, add a layer. Readers click a dot to jump to
its chapter, or Tab to it and use the arrow keys, Home and End. The current
chapter’s dot is larger and filled, so it stands out by more than colour.
markerColor fills the dots with a colour of your own.
showMarkers: truemarkerColor: "#f97316"The legend
Section titled “The legend”A story’s legend is pinned: it stays in one corner, above the chapter cards,
while the story scrolls. Write it as on a map, with title and items:
legend: title: Earthquakes collapsed: auto # auto (the default), true or false # position: top-right # optional; the default follows the cards items: - { color: "#dc2626", label: "Magnitude 4.5 or more", shape: circle }- Corner.
position:if you set one. Otherwise top-left (away from the chapter rail), or top-right on a wide screen whose cards are allalignment: left. - Open, or a button. With
collapsed: autothe legend is a “Legend” button on phones, and on wide screens where cards pass its corner (cards on both sides, or full-width). It is open everywhere else.truealways starts it as a button,falsealways open. The button’s word is translated bymaplibre-yaml.Legend.Titleinconfig.locale. - The panel. The button opens the legend above the cards. The page stays usable; Escape or (on a phone) a tap outside closes it.
A story with a classify: layer, or a layer with
its own legend: entry, gets a legend without a legend: block, by the same
rules. legend: false at the top turns it off. A slot="legend" child of the
story element fills the panel with your own markup instead.
Use the rest of a map document
Section titled “Use the rest of a map document”A story’s map is an ordinary map document, so the top-level keys a
type: map document takes work here too, with the same
rules: title: and description: (the map’s name and summary for screen
readers), named sources:, standalone markers: and popups:,
controls:, images:, the params panel (state: and parameters:), and
terrain:, sky:, light: and projection:. Write them at the top of the
story file, not under config:.
type: scrollytellingid: harbour-tourtitle: "New York harbour"config: mapStyle: "https://tiles.openfreemap.org/styles/liberty" center: [-74.02, 40.69] zoom: 12sources: piers: type: geojson locations: - { coordinates: [-74.017, 40.703], name: Battery Park } - { coordinates: [-73.997, 40.705], name: South Street Seaport }layers: - id: pier-dots type: circle source: piers paint: circle-radius: 8 circle-color: "#0ea5e9"markers: - at: [-74.0445, 40.6892] color: "#16a34a" popup: - h3: - str: Statue of Libertychapters: - id: piers title: "The piers" center: [-74.01, 40.70] zoom: 13 - id: liberty title: "Liberty Island" center: [-74.0445, 40.6892] zoom: 14The story map’s own pan and zoom gestures start off, so the reader’s wheel
and finger scroll the story. Markers, popups, the legend and the params panel
stay clickable. To let readers move the map, turn a gesture back on in
config: (scrollZoom: true, dragPan: true).
Theme, footer and the container
Section titled “Theme, footer and the container”theme: darkgives the cards light text on a dark background;lightis the default.footer:is HTML shown after the last chapter.className:adds classes to the story element, andstyle:adds inline CSS to it, for examplestyle: "--ml-scrolly-map-height: 80vh". The class names and custom properties you can style are in Styling.
theme: darkclassName: quake-storyfooter: "<p>Data: <a href=\"https://earthquake.usgs.gov/\">USGS</a></p>"How a story behaves
Section titled “How a story behaves”- The active chapter is the one whose card is on screen and nearest the middle of it, recomputed as the page scrolls. The story starts reacting once its first chapter reaches the middle 20% of the screen. On phones the map moves on once the next card is on screen.
- On load, the map opens on the
config:camera, then at once applies the chapter in that middle zone. On a page that starts with the story, the first chapter’s camera,layersandonChapterEnteractions all run. - Changing chapter runs the previous chapter’s
onChapterExitactions, moves the camera, applieslayers, then runsonChapterEnter. - Reading comes first. The wheel and touch scroll the story, not the map. The rail, legend, markers, popups and slots stay clickable. Nothing on the map moves with the scroll: the legend, controls and corner slots stay in their corners, under the cards.
- Reduced motion. Cards stop animating, every camera move and camera
action is instant (an action whose
optionssetessential: truestill animates), androtateAnimationandspinGlobeare off. - Problems. A story that fails to load or validate shows one card listing each problem with its line number, instead of the story. A story map that is slow to start shows “Loading map…” until its basemap draws.
A complete example
Section titled “A complete example”The past week’s earthquakes, coloured by magnitude, with the chapter rail, a legend and a chapter that filters to the strongest:
type: scrollytellingid: earthquake-storytitle: "A week of earthquakes"theme: darkshowMarkers: truemarkerColor: "#f97316"config: mapStyle: "https://tiles.openfreemap.org/styles/liberty" center: [0, 20] zoom: 1.5legend: title: Magnitude items: - { color: "#fbbf24", label: "4.5 to 6", shape: circle } - { color: "#dc2626", label: "6 or more", shape: circle }layers: - id: quakes type: circle source: type: geojson url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_week.geojson" paint: circle-radius: ["interpolate", ["linear"], ["get", "mag"], 4.5, 4, 8, 16] circle-color: ["step", ["get", "mag"], "#fbbf24", 6, "#dc2626"] circle-stroke-width: 1 circle-stroke-color: "#ffffff"chapters: - id: world title: "A week of earthquakes" description: "<p>Every earthquake of magnitude 4.5 or more in the past seven days.</p>" center: [0, 20] zoom: 1.5 alignment: full - id: ring-of-fire title: "The Ring of Fire" description: "<p>Most of them follow the edges of the Pacific.</p>" center: [160, 10] zoom: 2 alignment: left - id: japan title: "Japan" description: "<p>Four plates meet under Japan.</p>" center: [138, 37] zoom: 4.5 pitch: 40 alignment: right - id: strongest title: "The strongest" description: "<p>Only magnitude 6 and above.</p>" center: [0, 20] zoom: 1.5 onChapterEnter: - action: setFilter layer: quakes filter: [">=", ["get", "mag"], 6] onChapterExit: - action: setFilter layer: quakes filter: nullfooter: "<p>Data: USGS earthquake feed, updated every minute.</p>"To change this story and watch it update as you type, open it in the
playground. Download there
saves it as story.yaml with an index.html that shows it.
The repository’s
examples/scrollytelling
story uses every kind of action, on a plain HTML page.
Reference
Section titled “Reference”- YAML reference for stories: every key,
with its type, default and allowed values, generated from the schema the
validator uses. For example
chapter keys,
actions and
callback. <ml-scrollytelling>reference: the element’s attributes, events, slots and styling hooks.- Story maps in plain HTML and Story maps in Astro: putting a story on a page.
Troubleshooting
Section titled “Troubleshooting”Problems every map shares are in Troubleshooting.
**bold**shows literally in a chapter.descriptionis HTML; write<strong>bold</strong>.- “Two chapters have id …”. Each chapter needs its own
id; the message names both lines. Copying a chapter and keeping itsidis the usual cause. - A layer doesn’t appear or disappear. Check the chapter’s
layersids against the layers’ids. A repeated layeridwarns, and only the first layer with it is drawn. - A key under
config:does nothing, such aslegend:orsources:. These go at the top of the story file; the map’s notice says so while you develop. - The map scrolls away with the chapters. A wrapper around the story has
overflow: hidden,autoorscroll, which stopsposition: sticky.
Moved sections
Section titled “Moved sections”Showing a story
Section titled “Showing a story”Putting a story on a page is on Story maps in plain HTML and Story maps in Astro.
Scrollytelling properties
Section titled “Scrollytelling properties”The table of every key is the generated YAML reference for stories.