Skip to content

Scroll rail

A rail of ticks beside a long conversation, one per question, that shows where you are and jumps back to any turn.

  • <aparte-scroll-rail>
On this page
Open

A scrollbar says how far down you are, not which question you are under. The rail does: one tick per question, the current one longer, and a click jumps back to it. It is the minimap of a code editor reduced to its ticks, applied to turns.

It only reads the conversation: which messages exist, which one is in view, and the first words of each to name its tick. The transcript keeps its own scrolling and its own markup. With fewer than two ticks it shows nothing.

Open

every="message" marks replies too. The default marks only questions, because a tick per reply makes a long thread unreadable.

Place it as a direct child of <aparte-chat>: it floats along the transcript’s edge, clear of the scrollbar. Outside the chat, target names the chat it follows.

html
<aparte-scroll-rail data-empty=""></aparte-scroll-rail>
js
el.addEventListener('aparte-scroll-rail-jump', (e) => use(e.detail));

Installation and the framework-specific traps: React · Vue · Svelte · Angular.

Before a jump, it fires aparte-scroll-rail-jump, which is cancelable: an app that loads older history in pages can fetch the turn first, then scroll.

The rail takes the height of its ticks, up to a share of the transcript (--aparte-scroll-rail-share, 60%). When there are more turns than fit, the ticks draw closer together, then the rail scrolls itself so the current one stays in view. It is hidden on touch screens, where a thin tick is no target.

<aparte-scroll-rail> · class AparteScrollRail

Attributes

AttributeDescription
targetThe id of the <aparte-chat> to follow, when the element is not inside it.
everyuser (default): one tick per user turn. message: one per message.
data-emptyReflected BY the element while it has fewer than two ticks. Read-only.

Properties

PropertyTypeDescription
currentMessageId (readonly)string | nullThe id of the message the rail currently marks, or null.

Methods

MethodDescription
jumpTo(messageId: string): voidScroll the transcript to a message. Announced first, cancelable; then a scrollIntoView on the bubble, smooth unless the reader prefers reduced motion. The clicked tick is the mark until the transcript has stopped moving: while a smooth scroll crosses other questions the band would name each in turn, and it ended on the wrong one two times in three before this hold. Where the scroll lands is checked once it has settled, and corrected. The bubbles carry content-visibility: auto, so a bubble far from the reader has an estimated height until it is rendered: the scroll aims at an estimate, the bubbles above it take their real size on the way, and the message ends up above or below the top — measured from 36px to 1,213px off, in all three engines, the further the worse. A second scrollIntoView from a rendered neighbourhood lands; a few are allowed.

Events

EventTypeDescription
aparte-scroll-rail-jumpCustomEvent<AparteScrollRailJumpDetail>A tick was activated. Bubbles, cancelable: preventDefault() leaves the transcript where it is.

CSS variables

VariableDefaultDescription
--aparte-scroll-rail-widthmax(var(--aparte-scroll-rail-hit-size), calc(var(--aparte-scroll-rail-tick-size) * 1.6))The rail’s column, the ticks end-aligned in it. Never under one tick’s pressable zone: the rail clips, so a narrower column would cut the target back.
--aparte-scroll-rail-tick-size14pxLength of a tick; the current one is 1.6× that.
--aparte-scroll-rail-tick-thickness2pxThickness of a tick.
--aparte-scroll-rail-hit-size24pxThe pressable zone around a tick (WCAG 2.5.8). The drawn line keeps its own size; this one sizes the pseudo-element and, through the gap below, the pitch — so raising it spaces the ticks out rather than overlapping them. When the ticks would not fit, the rail sets it on itself to the pitch that does, never under 6px.
--aparte-scroll-rail-gapcalc(var(--aparte-scroll-rail-hit-size) - var(--aparte-scroll-rail-tick-thickness))Space between ticks: the zone minus the line, so gap + thickness is exactly the pitch. Set it smaller and the zones overlap.
--aparte-scroll-rail-share.6The share of the transcript’s height the rail may take. It is the height of its list up to this, centred on the transcript; past it the pitch tightens, then the rail scrolls.
--aparte-scroll-rail-bar0pxPublished BY the element: the width of the transcript’s scrollbar when it is a classic one (0 for an overlay bar), which the stylesheet adds to the rail’s end inset so the ticks never sit on the bar.
--aparte-scroll-rail-block-start0pxPublished BY the element: the distance from the host’s top edge to the transcript’s, so the rail centres on the transcript rather than on the composer too.
--aparte-scroll-rail-block-end0pxPublished BY the element: the distance from the transcript’s bottom edge to the host’s.
  • The rail is a navigation landmark named from the scrollRailLabel key (“Conversation outline”); each tick is a button named by the first words of its message.
  • The tick in view carries aria-current; arrow keys walk every tick.
  • A tick’s target is 24px tall, wider than the drawn line, as long as the rail has room for every tick (WCAG 2.5.8); past that, the ticks draw closer together.
  • Conversation — the transcript the rail follows.
  • Chat — where it is placed.