# StoryThread

> A narrative thread for scroll stories: the page pans sideways through your sections while a dotted line draws itself from node to node, a glowing head rides its tip, nodes light up as it reaches them, and segments can fork and meet again.

- Package: `fancy-ui-react` (React 18/19 + Tailwind CSS v4)
- Category: Navigation
- Tags: story, storytelling, scrollytelling, path, thread, timeline, horizontal scroll, svg, narrative
- Types: `StoryThreadProps`, `StoryNodeProps`, `StoryThreadSegment`, `StoryThreadLayout`, `StoryThreadDash`
- Docs: https://fancy-ui.rama.app/docs/components/story-thread
- Source: https://github.com/RamaHerbin/fancy-ui/tree/main/react/src/components/story-thread

## Import

```tsx
import { StoryThread } from 'fancy-ui-react';
```

Setup (once per project): `npm install fancy-ui-react`, then put these lines in your main CSS file (the stylesheet import goes right after `@import "tailwindcss";`):

```css
@import "tailwindcss";
@import "fancy-ui-react/tailwind.css";
```

and import the component styles once in the app entry: `import "fancy-ui-react/styles.css";`. Full guide: https://fancy-ui.rama.app/llms.txt

The names below are the ones to write in React; when one differs from the Svelte name, the Svelte name follows in parentheses. `class` becomes `className`, a callback takes the name the React port declares (`onclick` becomes `onClick`), snippets become `ReactNode` props (render functions when they take arguments) and the default snippet is `children`. Divergences: https://github.com/RamaHerbin/fancy-ui/tree/main/react#divergences-from-the-svelte-api

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `layout` | `"auto" \| "horizontal" \| "vertical"` | `"auto"` | `horizontal` pins a full-height viewport and pans the content sideways as the page scrolls; `vertical` keeps a plain stack with the thread running down it. `auto` is horizontal from 900 × 520 px up. |
| `segments` | `StoryThreadSegment[]` | `[]` | Segments as `{ nodes: string[], color? }`, each through a list of StoryNode ids. Empty draws one segment through every node in document order. Segments that leave the same node draw a fork whose branches advance together. |
| `lead` | `number` | `0.7` | Where the tip of the thread sits in the viewport, 0..1 along the reading axis. |
| `tension` | `number` | `0.55` | Curve tension between nodes: 0 draws straight lines, 1 long flat tangents. |
| `dash` | `"dotted" \| "dashed" \| "solid"` | `"dotted"` | Stroke pattern of the thread. |
| `head` | `boolean` | `true` | Draws a glowing dot at the tip of every segment while it is drawing. |
| `sound` | `boolean` | `false` | Plays a soft `tick` each time the thread reaches a node going forward. Only audible once the user has enabled sound. |
| `onReach` | `(id: string) => void` | — | Called with a node's id each time the thread reaches it going forward. |
| `ariaLabel` | `string` | `"Story"` | Accessible name of the region. |
| `className` (Svelte `class`) | `string` | — | Additional CSS classes on the root. |

## Node props

| Prop | Description |
| --- | --- |
| `children` | The story: sections holding `<StoryNode id>` anchors (`variant` `dot` or `ring`). Size the sections yourself; in the horizontal layout they sit side by side at full height. |

## Example

```tsx
import { StoryNode, StoryThread } from "fancy-ui-react";

const chapters = [
	{
		id: "start",
		n: "01",
		title: "Where it starts",
		text: "Scroll down: the page moves sideways and the thread follows.",
		top: "80%",
	},
	{
		id: "craft",
		n: "02",
		title: "Learning the craft",
		text: "Each node lights up as the thread reaches it.",
		top: "56%",
	},
	{
		id: "ship",
		n: "03",
		title: "Shipping it",
		text: "Wheel sideways, drag, or use the arrow keys.",
		top: "86%",
	},
	{
		id: "next",
		n: "04",
		title: "What comes next",
		text: "The end of the thread, so far.",
		top: "62%",
	},
];

export function StoryThreadExample() {
	return (
		<div className="bg-background text-foreground h-[420px] w-full overflow-y-auto rounded-xl border">
			<StoryThread layout="horizontal" ariaLabel="A short story">
				{chapters.map((chapter) => (
					<section key={chapter.id} className="relative h-full w-[520px] shrink-0 px-10 py-12">
						<p className="text-muted-foreground font-mono text-xs">( {chapter.n} )</p>
						<h3 className="mt-2 text-2xl font-semibold tracking-tight">{chapter.title}</h3>
						<p className="text-muted-foreground mt-2 max-w-[18rem] text-sm">{chapter.text}</p>
						<StoryNode id={chapter.id} className="absolute right-24" style={{ top: chapter.top }} />
					</section>
				))}
			</StoryThread>
		</div>
	);
}
```
