hypnagaga/src/components/HorizontalScroller/HorizontalScroller.mdx
2026-01-09 14:24:01 +05:30

403 lines
13 KiB
Text

import { Meta } from '@storybook/blocks';
import * as HorizontalScrollerStories from './HorizontalScroller.stories.svelte';
import IllustratorScreenshot from './assets/illustrator.png';
<Meta of={HorizontalScrollerStories} />
# HorizontalScroller
The `HorizontalScroller` component is helpful in making horizontal scrolling sections that respond to vertical scroll input. It is flexible in a way that it can horizontally scroll any children content wider than 100vw from one end to the other.
To scroll any DOM layout wider than the viewport, wrap the content inside the `HorizontalScroller` component. The component will take care of the rest.
## Basic demo
To use the `HorizontalScroller` component, import it and provide the children content to scroll. The scroll height defaults to `200lvh`, but you can adjust this to any valid CSS height value such as `1200px` or `200lvh` with the `height` prop.
> 💡TIP: Use `lvh` or `svh` units instead of `vh` unit for the height, as [these units](https://www.w3.org/TR/css-values-4/#large-viewport-size) are more reliable on mobile or other devices where elements such as the address bar toggle between being shown and hidden.
[Demo](?path=/story/components-graphics-horizontalscroller--demo)
```svelte
<script lang="ts">
import { HorizontalScroller } from '@reuters-graphics/graphics-components';
</script>
<!-- Optionally set `height` to adjust scroll height -->
<HorizontalScroller height="400lvh">
<div style="width: 200vw; height: 100lvh;">
<!-- Content wider than 100vw -->
<!-- Only the top 100lvh will be visible -->
<img
src="path/to/wide-image.jpg"
alt="alt text"
style="width: 100%; height: 100%; object-fit: cover; padding: 0; margin: 0;"
/>
</div>
</HorizontalScroller>
```
## With stops
The `HorizontalScroller` also allows you to define a set of points to stop or slow down the scrolling at specific intervals using the `stops` prop. This is useful for creating step-based horizontal scrolling experiences.
The `scrubbed` prop can be used to define whether the scrolling experience should be smooth or tied directly to the scroll position. Setting `scrubbed` to `true` will make the horizontal scroll position directly correspond to the vertical scroll position, while setting it to `false` will create a smooth scrolling effect.
If `scrubbed` is set to `false` and `stops` are defined, the scroller will transition smoothly to the next stop when the `Mapped Progress` reaches the midpoint between the two stops. The transition speed is controlled by the `duration` prop (in milliseconds) and the `easing` prop (which accepts any easing function from `svelte/easing` or a custom function based on signature `(t: number) => number`).
If `scrubbed` is set to `true` and `stops` are defined, all the stops are traversed at equal distance but based on the easing function provided.
Use `showDebugInfo` prop to visualize the scroll progress and other useful debug information. The `Progress` indicates the vertical progress with values in the range 0...1 indicating the content being locked or a user-fed value to control the horizontal scroll position. The `Mapped Progress` value indicates the vertical progress mapped to mappedStart and mappedEnd values. By default these are 0 and 1 respectively. Finally, the `Eased Progress` value indicates the horizontal scroll progress after applying stops and easing (if any). `Eased Progress` accurately reflects the transition of horizontal scroll position.
Feel free to toggle `scrubbed` prop here to see the difference.
[Demo](?path=/story/components-graphics-horizontalscroller--demo)
```svelte
<script lang="ts">
import { HorizontalScroller } from '@reuters-graphics/graphics-components';
import { quartInOut } from 'svelte/easing';
</script>
<HorizontalScroller
height="200lvh"
stops={[0.2, 0.5, 0.6, 0.7]}
duration={400}
scrubbed={false}
easing={quartInOut}
showDebugInfo
direction="right"
>
<div style="width: 200vw; height: 100lvh;">
<!-- Content wider than 100vw -->
<!-- Only the top 100lvh will be visible -->
<img
src="path/to/wide-image.jpg"
alt="alt text"
style="width: 100%; height: 100%; object-fit: cover; padding: 0; margin: 0;"
/>
</div>
</HorizontalScroller>
```
## Extended boundary
`HorizontalScroller` also provides `mappedStart` and `mappedEnd` props to extend the horizontal scroll boundaries beyond the default 0 to 1 range. This is useful when you want to create an overscroll effect or have more control over the horizontal scroll range. By default these values are set to 0 and 1 respectively.
[Demo](?path=/story/components-graphics-horizontalscroller--extended-boundary)
```svelte
<script lang="ts">
import { HorizontalScroller } from '@reuters-graphics/graphics-components';
import { quartInOut } from 'svelte/easing';
</script>
<HorizontalScroller
height="200lvh"
mappedStart={-0.5}
mappedEnd={1.5}
stops={[0, 1]}
scrubbed={true}
easing={quartInOut}
direction="right"
showDebugInfo
>
<div style="width: 200vw; height: 100lvh;">
<!-- Content wider than 100vw -->
<!-- Only the top 100lvh will be visible -->
<img
src="path/to/wide-image.jpg"
alt="alt text"
style="width: 100%; height: 100%; object-fit: cover; padding: 0; margin: 0;"
/>
</div>
</HorizontalScroller>
```
## With custom child components
You can create a horizontal stack of any components and pass it as children to the `HorizontalScroller`. Here's an example of using `DatawrapperChart`, `Headline` and ai2svelte components inside the scroller.
[Demo](?path=/story/components-graphics-horizontalscroller--custom-children)
```svelte
<script lang="ts">
import {
Block,
DatawrapperChart,
Headline,
HorizontalScroller,
} from '@reuters-graphics/graphics-components';
import AiChart from './ai2svelte/ai-chart.svelte';
import { quartInOut } from 'svelte/easing';
</script>
<HorizontalScroller
height="200lvh"
stops={[0.2, 0.5, 0.6, 0.7]}
duration={400}
scrubbed={false}
easing={quartInOut}
direction="right"
showDebugInfo
>
<div id="horizontal-stack">
<div style="width: 100vw;">
<DatawrapperChart
title="Global abortion access"
ariaLabel="map"
id="abortion-rights-map"
src="https://graphics.reuters.com/USA-ABORTION/lgpdwggnwvo/media-embed.html"
frameTitle=""
scrolling="no"
textWidth="normal"
width="wider"
/>
</div>
<div style="width: 100vw;">
<Headline
hed="Reuters Graphics Interactive"
dek="The beginning of a beautiful page"
section="World News"
/>
</div>
<div style="width: 100vw;">
<Block width="normal">
<AiChart />
</Block>
</div>
</div>
</HorizontalScroller>
<style lang="scss">
#horizontal-stack {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
gap: 10vw;
height: 100%;
}
</style>
```
## With ai2svelte components
With ai2svelte v1.0.3 onwards, you can export your ai2svelte graphic with a wider-than-viewport layout and use it directly inside the `HorizontalScroller` component to create horizontal scrolling graphics.
To do that, follow these steps:
1. In Illustrator, rename your artboard with a tag indicating breakpoint width for that artboard to be visible on page. For example, to make the XL artboard visible on viewports wider than 1200px, rename the artboard to `xl:1200`. You can have more than one artboard with different breakpoint widths.
2. In ai2svelte settings, set these properties and run ai2svelte to export the component.
```yaml
include_resizer_css: false
respect_height: true
allow_overflow: true
```
<img
src={IllustratorScreenshot}
alt="Screenshot showing Illustrator document with artboard panel"
/>
[Demo](?path=/story/components-graphics-horizontalscroller--scrollable-ai-2-svelte)
```svelte
<script lang="ts">
import { HorizontalScroller } from '@reuters-graphics/graphics-components';
import AiGraphic from './ai2svelte/ai-graphic.svelte';
import { sineInOut } from 'svelte/easing';
// If using with the graphics kit
import { assets } from '$app/paths';
</script>
<HorizontalScroller
width="fluid"
height="800lvh"
direction="right"
easing={sineInOut}
showDebugInfo
>
<AiGraphic assetsPath={assets} />
</HorizontalScroller>
```
## With ai2svelte components (advanced)
Binding the `progress` can be useful to even transition tagged content inside the ai2svelte graphic as part of the horizontal scrolling experience. For example, caption boxes exported as `htext` tagged layers can be animated to fade in/out or move in/out of view based on the scroll progress. Or one could even use tagged `png` layers to create parallax effects.
[Demo](?path=/story/components-graphics-horizontalscroller--scrollable-ai-2-svelte-advanced)
```svelte
<script lang="ts">
import { HorizontalScroller } from '@reuters-graphics/graphics-components';
import AiGraphic from './ai2svelte/ai-graphic.svelte';
import { sineInOut } from 'svelte/easing';
// If using with the graphics kit
import { assets } from '$app/paths';
// bind progress for advanced interactivity
let progress: number = $state(0);
let pngLayer: HTMLElement | null;
let captions: HTMLElement[] | null;
let threshold = 0.8;
let screenWidth: number = $state(0);
function handleScroll() {
// to create the parallax movement
if (pngLayer) {
pngLayer.style.transform = `translateX(${map(progress, 0, 1, -400, 400)}px)`;
}
// for each caption, checks if position of the caption < 80vw
// if it is, show it
// if not, hide it
if (captions?.length) {
captions.forEach((caption) => {
let captionWidth = caption.getBoundingClientRect().width;
let captionMidpoint =
caption.getBoundingClientRect().left + captionWidth / 2;
if (
captionMidpoint < screenWidth * threshold &&
caption.style.opacity !== '1'
) {
caption.style.opacity = '1';
} else if (
captionMidpoint > screenWidth * threshold &&
caption.style.opacity !== '0'
) {
caption.style.opacity = '0';
}
});
}
}
// refetch new captions and png image
// every time the artboard changes
function onArtboardChange(artboard: HTMLElement) {
pngLayer = artboard.querySelector('.g-png-layer-overlay');
captions = Array.from(artboard.querySelectorAll('.g-captions'));
if (pngLayer) {
window.removeEventListener('scroll', handleScroll);
window.addEventListener('scroll', handleScroll, {
passive: true,
});
}
}
</script>
<HorizontalScroller
width="fluid"
height="800lvh"
direction="right"
bind:progress
easing={sineInOut}
showDebugInfo
>
<AiGraphic assetsPath={assets} {onArtboardChange} />
</HorizontalScroller>
<style lang="scss">
:global(.scroller-caption) {
padding: 1rem;
margin: 0;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 8px;
filter: drop-shadow(0px 2px 16px rgba(0, 0, 0, 0.2));
}
</style>
```
## With ScrollerBase
You can also integrate HorizontalScroller with `ScrollerBase` for a horizontal scroll with vertical captions experience.
[Demo](?path=/story/components-graphics-horizontalscroller--scrollable-ai-2-svelte)
```svelte
<script lang="ts">
import {
HorizontalScroller,
ScrollerBase,
} from '@reuters-graphics/graphics-components';
import AiGraphic from './ai2svelte/ai-graphic.svelte';
import { circInOut } from 'svelte/easing';
import { circInOut } from 'svelte/easing';
// Optional: Bind your own variables to use them in your code.
let count = $state(1);
let index = $state(0);
let offset = $state(0);
let progress = $state(0);
let top = $state(0);
let threshold = $state(0.5);
let bottom = $state(1);
</script>
<ScrollerBase
{top}
{threshold}
{bottom}
bind:count
bind:index
bind:offset
bind:progress
query="div.step-foreground-container"
>
{#snippet backgroundSnippet()}
<!-- Make sure to set height to `100lvh` -->
<!-- and handleScroll to false to avoid scroll conflicts -->
<HorizontalScroller
width="fluid"
height="100lvh"
direction="right"
bind:progress
scrubbed
stops={[0.5]}
handleScroll={false}
easing={circInOut}
showDebugInfo
>
<Demo />
</HorizontalScroller>
{/snippet}
{#snippet foregroundSnippet()}
<!-- Add custom foreground HTML or component -->
<div class="step-foreground-container"><p>Step 1</p></div>
<div class="step-foreground-container"><p>Step 2</p></div>
<div class="step-foreground-container"><p>Step 3</p></div>
<div class="step-foreground-container"><p>Step 4</p></div>
<div class="step-foreground-container"><p>Step 5</p></div>
{/snippet}
</ScrollerBase>
<style lang="scss">
.step-foreground-container {
height: 100vh;
width: 50%;
background-color: rgba(0, 0, 0, 0);
padding: 1em;
margin: 0 auto 10px 0;
position: relative;
left: 50%;
transform: translate(-50%, 0);
p {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
padding: 1rem;
background-color: rgba(0, 0, 0, 0.8);
border-radius: 4px;
color: white;
}
}
</style>
```