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
Available for Svelte 5, React, and Vue 3.
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";
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>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]>) |
Links
Related components
ContainerScroll
Scroll-driven aperture: the card opens from a bright seam as it enters the viewport while its content settles into focus and the title blurs away, with no 3D tilt
Layout Fancy
Marquee
An eased conveyor of repeated content that dissolves into soft edge fades and, on hover or focus, brakes smoothly to a stop and pulls away again instead of freezing.
Layout Fancy