createScroll and options
@annetaan/leko/scroll exports createScroll and the types it takes and hands
back. Leko Scroll is a product of its own, not a kind of tour: it has no story,
no steps and nothing to wait for, and it lights the targets of a long page one
after another as the reader scrolls —
A second product, not a second tour.
createLeko and createScroll know nothing of each other. Each draws a scrim
of its own, and not running both at once is the page’s to decide.
createScroll
Section titled “createScroll”createScroll(options: LekoScrollOptions): LekoScrollIt runs from the moment it is called. There is no start and no stop: the
light follows the scroll for as long as the page is up, and
destroy() ends it. If it throws, it leaves no scrim and no
listener behind —
The API.
import { createScroll } from '@annetaan/leko/scroll'
const scroll = createScroll({ targets: [ { target: '#hero' }, { target: '#pricing', message: { title: 'Pricing', body: 'Every plan, side by side.' }, }, { target: '#quotes', off: true }, { target: '#faq', side: 'top', padding: 16 }, ], onChange: (index) => console.log('lit', index),})Nothing it puts over the page catches a pointer, so every link, button and field under the scrim answers as though it were not there. There is no next control, no close control and no focus ring — The whole page stays usable.
With prefers-reduced-motion set when it is called, createScroll draws
nothing and writes one console.info. It still hands back a LekoScroll,
whose measure() and destroy() do nothing, and onChange is never called,
so the page needs no branch for it. The setting is read once, and a later
change is not followed —
Reduced motion.
LekoScrollOptions
Section titled “LekoScrollOptions”The object createScroll takes. Only targets has to be given. padding and
radius here are for every target that does not set its own.
| Option | Type | Default |
|---|---|---|
targets |
readonly LekoScrollTarget[] |
required |
line |
number |
0.5 |
fade |
number |
300 |
spacing |
number |
150 |
padding |
number |
8 |
radius |
number |
8 |
intro |
LekoScrollIntro |
— |
halo |
'arrive' | 'follow' |
'arrive' |
onChange |
function | — |
targets
Section titled “targets”What gets lit, in the order it gets lit. Each entry is a
LekoScrollTarget. The order is the array’s and is never
sorted into page order, so a target written first is lit first even where it
sits lower on the page, and a disagreement between the two draws no warning —
The array is the order.
Where on the screen the switch happens, as a fraction of the viewport’s height from the top. A target switches on when its top edge comes up to the line, and the one lit is always the one the line last passed — The line and the switch.
How far past an edge the light takes to go out, in px: scrolling back above the first target, on past the last, or into a stretch where the light is off. Leaving fades with the scroll rather than on time, so scrolling back mid-fade gives the opacity back as it goes — Entering converges, leaving fades and The two edges.
spacing
Section titled “spacing”The least scroll between two switches, in px. Two targets close together on the page are kept at least this far apart, and the switches are pulled up so that every target near the foot of the page is lit on the way down — The line and the switch.
padding
Section titled “padding”The space between a target’s border box and the edge of its hole, in px. A
target’s own padding wins over this one —
Padding and radius.
radius
Section titled “radius”The corner radius of the hole, in px. A target’s own radius wins over this
one —
Padding and radius.
The converge createScroll starts, when the page is already on a target as it
is called: the dark closing in on that target before the reader has scrolled.
It is a LekoScrollIntro, and it changes nothing about the
converges later scrolls start —
Entering converges, leaving fades.
What the halo does while the dark closes
in on a target. Under 'arrive' it is absent until the hole arrives, and
appears then. Under 'follow' it rides the hole in and grows to full strength
as the hole lands. Either holds for every converge, the intro and the ones a
scroll starts alike —
The halo.
onChange
Section titled “onChange”onChange?: (index: number | undefined) => voidThe one notice. It fires with a target’s index in targets when the light
arrives at it, and with undefined when nothing is lit at creation or when the
light goes out after an index was announced. The one at creation comes after
createScroll has returned, so the handler can use the LekoScroll it
returned, and a destroy() before then means it is not called. A target a fast
scroll skips never fires. The return value is never read —
The whole page stays usable.
LekoScrollTarget
Section titled “LekoScrollTarget”type LekoScrollTarget = LekoScrollLit | LekoScrollOffOne entry of targets: one that gets lit, or a marker where the light goes
off.
LekoScrollLit
Section titled “LekoScrollLit”interface LekoScrollLit { target: string | Element message?: LekoScrollMessage side?: LekoScrollSide padding?: number radius?: number off?: false}| Field | Type | Required |
|---|---|---|
target |
string | Element |
yes |
message |
LekoScrollMessage |
no |
side |
LekoScrollSide |
no |
padding |
number |
no |
radius |
number |
no |
off |
false |
no |
target is a selector or an element, laid out in the normal flow of the
document. A target that is, or sits inside, something position: sticky or
position: fixed, or that sits inside a scroller of its own, is not supported,
because the scrim rides the document’s scroll and those do not.
A selector that matches several elements means the first of them in document
order that has a box, and the match is chosen again each time the page is
measured. A target that is not found is skipped with a console.warn, and the
rest runs; a string that is not a valid selector counts as not found. It is
looked for again the next time the page is measured, and its place in the
array is kept, so onChange still names every target by the index it was
written at —
What a target is.
message and side are the box beside the hole and where it goes.
padding and radius are this target’s own, over the ones in the options.
LekoScrollOff
Section titled “LekoScrollOff”interface LekoScrollOff { target: string | Element off: true}A marker: from its switch position the light is off, until the next lit
entry’s. It takes nothing but a target, because nothing about it is drawn. It
is found and measured like any other entry and is never lit, so its index is
never announced, and the light going out at it fires undefined —
A stretch where the light is off.
LekoScrollMessage
Section titled “LekoScrollMessage”interface LekoScrollMessage { title?: string body?: string}The box beside a lit target, shown only on a target that has one. A part left empty is not drawn, and a message with neither part shows no box. It is hidden while the hole moves and appears once it arrives, and it scrolls and dims with the scrim.
It catches no pointer, so its text cannot be selected and a link in it cannot
be pressed. Anything to act on belongs on the page —
The message.
Its look is set with the
--leko-scroll-message-* custom properties.
LekoScrollSide
Section titled “LekoScrollSide”type LekoScrollSide = 'top' | 'bottom' | 'left' | 'right'Which side of the hole the message goes on, 'bottom' when a target does not
say. Vertical room is never checked, so a box may sit off screen. A 'left' or
a 'right' that does not fit the viewport’s width goes to 'bottom' —
The message.
The space between the hole and the box is
--leko-scroll-message-gap.
LekoScrollIntro
Section titled “LekoScrollIntro”interface LekoScrollIntro { duration?: number}How long the converge createScroll starts takes, in ms. Left out, or with no
duration, it takes the 320ms every converge takes. A switch crossed during it
turns it into an ordinary converge —
Entering converges, leaving fades.
LekoScroll
Section titled “LekoScroll”interface LekoScroll { measure(): void destroy(): void}What createScroll hands back.
measure
Section titled “measure”Measures the page again. Leko Scroll measures on its own when it is created,
on a window resize, on load and once document.fonts.ready settles, and not
while the reader scrolls. It does not watch the layout, so a page that moves
its own layout, opening an accordion or loading a section, calls this once it
has —
Measuring.
document.querySelector('details')?.addEventListener('toggle', () => scroll.measure())With the same target lit, the hole is put back where the target now is, at once and with no notice. Where the lit target changed, it goes there the way a scroll to the same position would.
destroy
Section titled “destroy”Ends it. The scrim, the halo and the message come off the page, the listeners
go, and onChange is not called again. After it, measure() and destroy()
do nothing.