Trail

A contents rail for long pages. Collapsed it is the article at scale, measured in reading time, with a violet dot at the reading position. Open, it is a chart of the sections: each bar is a section's reading time, and the bar being read fills as you read. A violet line across the page marks where reading is measured.

Overview

Trail sits beside the text and shows how far the reader has come and how much is left. Every mark on it stands for something in the article: a tick is a share of its reading time, a longer tick is the start of a section, a darker tick has been read.

The four switches marked v4 turn on and off the parts added in this version, so each one can be compared with version 3.

Version 4

Duration bars

Collapsed, tick length shows the heading level. Open, the indent of the titles already shows the level, so the tick beside each title shows the section's reading time instead. Open, the ticks thicken to 2 px bars with round ends. Bar length is proportional to time. The scale is set so that the 90th percentile of section times reaches 24 px, and the few longer sections stop at 24 px, so one very long section does not shrink every other bar to a dot. No bar is shorter than 2 px.

Bars of sections already read are ink 4 and bars ahead are a35. The bar of the section being read fills with ink as far as the reader has come in it, so the open list is also a progress bar for the current section. The bars grow from level to time in the same spring that opens the panel. Titles start at 50 px to make room for them.

Sweep

Collapsed, the tick below the dot fills from the left in the read colour as the dot moves towards it, and is full when the dot reaches it. Progress moves continuously instead of one tick at a time. Only that tick changes on each frame.

Edge fade

When the open list is taller than the panel, the end with more titles beyond it fades out. The fade is as long as the distance to the end of the list, up to 48 px, two rows. It is eased, so the outer row is almost gone and the one inside it is faint, and the fade shrinks to nothing as the list reaches that end. It is a mask on the list, so nothing is drawn over the titles.

Placement

Trail does not place itself. On a reading page, such as an article or the Field Guide, it goes in the margin of the text column, so it reads as a map of that text and the eye does not travel far. In the app, it goes 16 px from the left edge of the content pane, beside the sidebar, so it keeps its place whatever the width of the text, and the panel and the skim label open into empty margin. It stays on the left in both, away from the scrollbar on the right.

Skim

With the pointer on the collapsed rail, the ticks of the section under it go one step darker, and a label beside the rail gives its title and reading time. The label follows the pointer from section to section on a spring. A click goes to that section.

Moving along the rail skims it. The panel opens when the pointer rests for 700 ms, which leaves time to read the label first.

Reading line

Trail decides what is being read at one line across the window, 15 percent from the top. Version 5 draws that line. It is static or shown with motion, and four parts can be added to either. The two combinations are the ones that work best together.

Motion

The line fades in over 160 ms when the page scrolls and fades out over 600 ms after it stops. It is there while the eye is looking for its place and gone while reading.

Mark

When a heading crosses the line, the whole line flashes and fades over 900 ms. That is the moment the rail moves to the next section, so the flash shows what decided it.

Fill

A stronger segment grows from the left edge of the text column to its right edge as the current section is read, measured in reading time like the rail. It starts again at the next heading. The rest of the line drops to a fainter violet so the segment stands out.

Drag

A small circle at the right end of the line moves it up or down, between 5 and 60 percent of the window, with the pointer or with the arrow keys. Everything in Trail follows, since the line defines the reading position. The position is kept in the reader's browser.

Dim

Blocks of text that have passed above the line fade to 36 percent. The paragraph the line is in, and everything below it, stays at full strength.

Defaults and settings

Trail has two sets of defaults. Read is for an article, the Field Guide or a document opened to read: the line is Guide and the rail sits by the text. Write is for an editor, in the docs and in the app: the line is off, because the cursor is where the writer is, a line across the text would compete with the cursor and the selection, and dim would fade the paragraph being edited. The rail stays in both, at the edge of the pane when writing, where it works as a navigator.

The top line of the open rail has a settings button beside the time left. It opens the same choices as the switches on this page: off, static or motion, then mark, fill, drag and dim. The settings close with the rail: when the pointer has left both for 400 ms, on a click outside, or with Escape. A reader's choice is kept in their browser, separately for reading and for writing, and Reset to default appears as soon as it differs from the defaults.

Combinations

The line is off, static or shown with motion. Static keeps it visible at all times, motion shows it only while the page scrolls. Mark, fill, drag and dim add to either. Guide is motion with mark and fill, and is the one to use by default: the line appears only while scrolling, shows how far into the section the reader is and marks each change of section. Focus is static with all four parts, for slow reading of long text: the line stays, read text recedes and the reader sets the line where their eyes read.

Collapsed state

Ticks sit 8 px apart and start 12 px from the left edge. The rail is half the height of the window. Nothing else is drawn in this state: no rail line, no labels, no background. The footprint in the layout is 26 px wide, so the text column never moves.

Reading time

Each section gets as many ticks as its share of the reading time. Text counts at 238 words per minute, the average for silent reading of non-fiction in Brysbaert's 2019 meta-analysis. An image or figure counts 12 seconds, then one second less for each one after it, down to 3 seconds, which is the rule Medium publishes. Code counts at half the speed of text.

Figure placeholder, 280 px tall
This figure is taller than the two paragraphs around it, but it counts as 12 seconds, about three lines of text. Switch the measure to Length to see the rail give it more ticks than the text.

Every heading keeps at least one tick of its own. When sections are too short for that, the headings are pushed apart and the ticks after them absorb the difference.

Read and unread

Ticks behind the dot are one step darker than ticks ahead of it: line 4 to a35 between headings, a35 to ink 4 on headings. The heading tick of the section being read is ink and 1.5 px thick. The colour follows the dot, not the scroll position, so the dot and the darker ticks never disagree while the spring settles.

The dot

The dot uses the accent glyph token, violet 500 in light and violet 350 in dark. It is 5 px and runs in its own lane, 5 px left of the ticks. It only moves vertically.

It follows the reading position through a spring with stiffness 223.8 and damping 28.42, which settles in about 350 ms with no visible overshoot.

Expanded state

The ticks between headings fade out, rows grow to 24 px and titles fade in from the top down. The dot moves into the list and sits left of the current title, in the same lane. The panel is as wide as the widest title plus padding, capped at 300 px.

Time left

The first line of the open panel is the reading time left from the reading line to the end of the article, in whole minutes rounded up. Under a minute it says so, and at the end of the page it says Finished.

Hover and focus

With skim on, the panel opens when the pointer rests on the rail for 700 ms. With skim off, it opens on intent: when the pointer slows below 0.3 px per ms inside it, or has been inside it for 90 ms. Either way, a pointer that crosses the rail on the way to the text does not open it. It closes 120 ms after the pointer leaves.

On touch screens a tap anywhere on the rail opens it, a tap on a title follows the link and closes it, and a tap outside closes it. Skim needs a pointer that can hover, so it is off on touch. On a phone the rail keeps a 26 px column beside the text.

Keyboard

The list is one tab stop. Tab lands on the section being read, not the first one. Up and Down move between titles, Home and End jump to the first and last, Enter follows the link and Escape closes the rail.

Scroll tracking

A section runs from its heading to the next heading, and the last one runs to the end of the article. The reading line sits 15 percent down the window. Which section is current is decided by the headings on the page. How far into it the reader is comes from reading time.

Tokens

Trail reads only system tokens: paper and raised for surfaces, ink and ink 2 to ink 4 for text, bars and read headings, a35 and line 4 for ticks, line 4 for the rings, e2 for the shadows and accent glyph for the dot.

Accessibility

The rail is a nav landmark named On this page with real links. The current link carries aria-current="location". The ticks between headings, the dot and the skim label are hidden from the accessibility tree. Open rows are 24 px tall.

Reduced motion

With reduced motion the rail opens without animation, the dot and the skim label jump instead of travelling, and the sweep still fills, since it moves only as fast as the reader scrolls.

Integration

const trail = new Trail(nav, {
  headings: '#doc h2, #doc h3',
  article: '#doc',
  measure: 'time',   // or 'length'
  wpm: 238,
  length: 0.5,       // rail length, share of the window
  bars: true, sweep: true, fade: true, skim: true,
  preset: 'read',    // or 'write' in an editor
});
trail.set('skim', false);

Resizes, font loading and changes in the height of the article are picked up by a ResizeObserver. Call trail.refresh() after the headings are replaced.

Long articles

With many headings the rail grows past half the window, up to one and a half ticks per heading. It never grows taller than the space from its sticky top to 24 px above the bottom of the window. When one tick per heading does not fit at 8 px, the ticks move closer, down to 3 px. The open list scrolls inside the panel without a scrollbar, since the edge fade already shows there is more, and keeps the current row in the middle until the reader scrolls it or moves through it with the keys.