Layout Fancy Stable
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
Preview
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>Installation
pnpm add fancy-ui-svelte
import { StickyScroll } from 'fancy-ui-svelte'
Usage
svelte
<script lang="ts">
import { StickyScroll } from 'fancy-ui-svelte';
</script>
<StickyScroll />Examples
Basic Usage
svelte
<script lang="ts">
import { StickyScroll } from "$lib/fancy-ui/sticky-scroll";
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>Props
| Prop | Type | Default | Description |
|---|---|---|---|
items * | 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 |
Slots
| Slot | 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]>) |