# StickyScroll

> Two-column scroll narrative with a scrolling item list and a sticky panel that swaps its content to the item centred in view, stacking to a single top-sticky panel over the items on narrow containers.

- Package: `fancy-ui-svelte` (Svelte 5 + Tailwind CSS v4)
- Category: Layout
- Tags: scroll, layout, sticky, narrative, scrollytelling, micro-interaction
- Types: `StickyScrollProps`
- Docs: https://fancy-ui.rama.app/docs/components/sticky-scroll
- Source: https://github.com/RamaHerbin/fancy-ui/tree/main/src/lib/fancy-ui/sticky-scroll

## Import

```svelte
<script lang="ts">
  import { StickyScroll } from 'fancy-ui-svelte';
</script>
```

Setup (once per project): `npm install fancy-ui-svelte`, then add `@import "fancy-ui-svelte/tailwind.css";` after `@import "tailwindcss";` in your main CSS file. Full guide: https://fancy-ui.rama.app/llms.txt

React: `import { StickyScroll } from 'fancy-ui-react';` (`class` becomes `className`, snippets become `ReactNode`, DOM callbacks are camel-cased; exact names on https://fancy-ui.rama.app/docs/components/react/sticky-scroll.md).

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` (required) | `T[]` | — | Required. The items rendered down the scrolling column. |
| `activeIndex` | `number` | `0` | Bindable. The active item's index. Holds its last value when nothing intersects the centre line. |
| `panelSide` | `"start" \| "end"` | `"end"` | Which logical side the panel sits on. Flips physically under dir="rtl". |
| `crossfade` | `boolean` | `true` | Whether the panel crossfades between items. Effective value is always false under reduced motion. |
| `panelClass` | `string` | — | Additional CSS classes for the sticky panel wrapper. |
| `panelHidden` | `boolean` | `true` | Whether the panel is aria-hidden. Set false when the panel holds content found nowhere else. |
| `onChange` | `(index: number, item: T) => void` | — | Called only when the active index actually changes. |
| `class` | `string` | — | Additional CSS classes for the root |
| `ref` | `HTMLDivElement \| null` | `null` | Bindable reference to the root element |

## Snippets

| Snippet | Description |
| --- | --- |
| `item` | Renders one row, given the item, its index, and whether it's the active one: (item, index, active). (Snippet<[T, number, boolean]>) |
| `panel` | Renders the sticky panel's content for the active item: (item, index). (Snippet<[T, number]>) |

## Examples

### Basic Usage

```svelte
<script lang="ts">
	import { StickyScroll } from "fancy-ui-svelte";

	interface Step {
		eyebrow: string;
		title: string;
		body: string;
		gradient: string;
	}

	const steps: Step[] = [
		{
			eyebrow: "Step 1",
			title: "Compose",
			body: "Start from a blank canvas or a saved template. Every keystroke auto-saves, so a dropped connection never costs you the draft, and attachments upload quietly in the background while you keep writing.",
			gradient: "from-sky-400 to-blue-600",
		},
		{
			eyebrow: "Step 2",
			title: "Review",
			body: 'A quiet second pass catches what a first draft always misses — a broken link, a missing attachment, a subject line that still says "Draft". Reviewers leave inline notes without ever blocking the sender.',
			gradient: "from-violet-400 to-purple-600",
		},
		{
			eyebrow: "Step 3",
			title: "Send",
			body: "One click hands it to the queue, not the network — retries, rate limits, and delivery receipts are all handled for you. The moment it lands, the thread updates for everyone already watching it.",
			gradient: "from-emerald-400 to-teal-600",
		},
	];
</script>

<div class="w-full p-6">
	<StickyScroll items={steps}>
		{#snippet item(step, index, active)}
			<div
				class="flex min-h-[22rem] flex-col justify-center gap-3 transition-opacity duration-300"
				class:opacity-40={!active}
			>
				<span class="text-muted-foreground text-xs font-semibold tracking-wide uppercase"
					>{step.eyebrow}</span
				>
				<h3 class="text-foreground text-2xl font-semibold">{step.title}</h3>
				<p class="text-muted-foreground max-w-md leading-relaxed">{step.body}</p>
			</div>
		{/snippet}
		{#snippet panel(step)}
			<div
				class="flex h-64 w-full items-center justify-center rounded-xl bg-gradient-to-br {step.gradient}"
			>
				<span class="text-2xl font-semibold text-white/90">{step.title}</span>
			</div>
		{/snippet}
	</StickyScroll>
	<p class="text-muted-foreground mt-6 text-xs">
		Scroll this panel — the sticky preview swaps to match whichever step is centered in view.
	</p>
</div>
```
