Liking cljdoc? Tell your friends :D

x-scroll-story

A scrollytelling web component. A sticky media panel stays pinned while text/content sections ("steps") scroll past it. Each step triggers a state change event, allowing consumers to update the media in response.

Tag name

<x-scroll-story>
  <div slot="media"><!-- sticky media content --></div>
  <section>Step 1 content</section>
  <section>Step 2 content</section>
  <section>Step 3 content</section>
</x-scroll-story>

Attributes

AttributeTypeDefaultDescription
layoutenum"left"Media panel position: "left", "right", "top"
thresholdnumber"0.5"Viewport fraction (0–1) where a step becomes active. 0.5 = center of viewport
splitnumber"0.5"Media/steps width ratio (0.1–0.9). Ignored when layout="top"
disabledbooleanfalseFreezes step tracking and suppresses events
labelstring""Accessible label for the region (aria-label)
autoplaybooleanfalseEnables automatic scrolling at a steady speed
autoplay-speednumber50Auto-scroll speed in pixels per second (clamped 1–1000)
autoplay-loopbooleanfalseLoop back to the start when the story ends
autoplay-indicatorbooleanfalseShow a pause icon overlay when auto-scroll is paused

Properties

PropertyTypeReflects attributeNotes
layoutstringlayout
thresholdnumberthresholdParsed and clamped to [0,1]
splitnumbersplitParsed and clamped to [0.1,0.9]
disabledbooleandisabled
labelstringlabel
activeIndexnumberRead-only. Index of active step (-1 if none)
progressnumberRead-only. Overall scroll progress [0,1]
autoplaybooleanautoplay
autoplaySpeednumberautoplay-speedParsed and clamped to [1,1000]
autoplayLoopbooleanautoplay-loop
autoplayIndicatorbooleanautoplay-indicator
autoplayPausedbooleanRead-only. Whether auto-scroll is currently paused

Events

EventBubblesComposedCancelableDetail
x-scroll-story-step-changeyesyesno{ index, id, previousIndex, previousId }
x-scroll-story-step-enteryesyesno{ index, id, progress }
x-scroll-story-step-leaveyesyesno{ index, id, progress }
x-scroll-story-progressyesyesno{ progress, activeIndex, activeId }
x-scroll-story-enteryesyesno{ progress }
x-scroll-story-leaveyesyesno{ progress }
x-scroll-story-autoplay-pauseyesyesno{ progress, activeIndex, activeId }
x-scroll-story-autoplay-resumeyesyesno{ progress, activeIndex, activeId }

The id field is the step element's id attribute if present, otherwise null.

Slots

SlotDescription
mediaSticky media panel content
(default)Step children. Each direct child is treated as one step.

Parts

PartDescription
containerOuter flex wrapper
mediaSticky media panel
stepsScrolling steps column
liveVisually-hidden aria-live announcement region
indicatorPause icon overlay (visible only when autoplay-indicator is set and autoplay is paused)

CSS Custom Properties

PropertyDefaultDescription
--x-scroll-story-media-width(from split)Overrides the split attribute for media width
--x-scroll-story-gap0Gap between media and steps panels
--x-scroll-story-step-min-height80vhMinimum height of each step section
--x-scroll-story-step-padding2remPadding inside each step
--x-scroll-story-active-opacity1Opacity of the active step
--x-scroll-story-inactive-opacity0.3Opacity of inactive steps
--x-scroll-story-transition-duration300msOpacity transition duration
--x-scroll-story-disabled-opacity0.55Opacity when disabled is set
--x-scroll-story-media-top0Top offset for sticky media positioning

Step activation

The component computes a trigger line at viewport-height * threshold from the top of the viewport. A step is active when it spans this trigger line. If no step spans the line, the last step whose top is above it is active.

The component sets a data-active attribute on the active step child (light DOM) and removes it from the previous active step. It also sets a data-active-index attribute on the host element reflecting the current active step index. This allows consumer CSS targeting:

x-scroll-story > section[data-active] {
  /* custom active styles */
}

x-scroll-story[data-active-index="0"] {
  /* styles when the first step is active */
}

Autoplay

When the autoplay attribute is present the component automatically scrolls the page at a steady rate controlled by autoplay-speed (pixels per second, default 50). Auto-scrolling only runs while the component is in the viewport and respects the user's prefers-reduced-motion setting.

Pause / resume

Users can temporarily pause auto-scrolling by:

  • Holding the mouse button (or touch) on the component — releasing resumes.
  • Holding the Space key while the component is focused — releasing resumes.

Moving the cursor outside the component while the mouse is held also resumes, preventing a stuck pause state.

Pause and resume fire x-scroll-story-autoplay-pause and x-scroll-story-autoplay-resume events respectively.

Looping

When autoplay-loop is set the story instantly jumps back to its start once progress reaches the end, creating an infinite loop.

Visual indicator

Set autoplay-indicator to display a subtle pause icon centered on the media panel whenever auto-scroll is paused.

Interaction with disabled

Setting disabled stops auto-scrolling. Removing disabled restarts it if autoplay is still present.

Accessibility

  • Container has role="region" with aria-label from the label attribute.
  • An aria-live="polite" region announces step changes to screen readers.
  • @media (prefers-reduced-motion: reduce) disables opacity transitions.

Usage

HTML

<x-scroll-story layout="left" threshold="0.5" split="0.4" label="Product story">
  <div slot="media">
    <img id="story-img" src="step1.jpg" alt="Product" />
  </div>
  <section id="intro">
    <h2>Introduction</h2>
    <p>Welcome to our product story...</p>
  </section>
  <section id="features">
    <h2>Features</h2>
    <p>Discover what makes this special...</p>
  </section>
  <section id="conclusion">
    <h2>Conclusion</h2>
    <p>Ready to get started?</p>
  </section>
</x-scroll-story>

JavaScript

const story = document.querySelector('x-scroll-story');

story.addEventListener('x-scroll-story-step-change', (e) => {
  const { index, id } = e.detail;
  const img = document.getElementById('story-img');
  img.src = `step${index + 1}.jpg`;
});

Constraint

position: sticky requires that no ancestor between the component and its scroll container has overflow: hidden, overflow: auto, or overflow: scroll. Ensure the component is not inside an overflow-constrained container.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close