# Stories

> An Instagram-style story viewer that grows out of the tapped avatar: segmented bars fill at the top, tap right or left to step, hold to pause while the chrome steps aside, swipe sideways to the next person with a plain slide, and drag down to send it back into the avatar.

- Type: Component (overlays)
- Access: Free, open source (MIT)
- Page: https://goodui.dev/native/components/stories
- Markdown: https://goodui.dev/native/components/stories.md
- Registry item: https://goodui.dev/native/r/stories.json
- Dependencies: react-native-gesture-handler, react-native-reanimated, react-native-svg
- Keywords: react native stories, instagram stories, story viewer, status viewer, segmented progress bar, tap to advance, hold to pause, swipe down to dismiss, expo stories

## When to use

- Short-lived updates from people or brands: social apps, community apps, creator updates.
- Onboarding or release highlights that play one card after another and can be skipped.
- Daily recaps: a week of workouts, a trip, order updates shown as cards.

## When not to use

- Browsing photos at the person's own pace with zoom: use image-viewer.
- Swipeable cards inside a screen: use carousel.
- Required onboarding steps with inputs: use a normal screen flow.

## Installation

With the shadcn CLI (needs a components.json in the Expo app), then `npx expo install --fix` so native dependencies match your Expo SDK. The foundation comes along the first time.

```bash
npx shadcn@latest add https://goodui.dev/native/r/stories.json
npx expo install --fix
```

The component lands in `components/goodui-native/stories/` and imports the foundation from `components/goodui-native/foundation/` with relative paths; it also installs avatar, locale.

Built-in strings follow `<GooduiLocaleProvider>` (one [language pack](https://goodui.dev/native/r/locale-tr.json) translates the whole kit); the `labels` prop overrides them for one instance.

## Usage

```tsx
import { useRef, useState, type ComponentRef } from "react";
import { View } from "react-native";
import { Avatar } from "@/components/goodui-native/avatar/avatar";
import { Stories, type StoryUser } from "@/components/goodui-native/stories/stories";

const users: StoryUser[] = [
  {
    id: "maya",
    name: "Maya Patel",
    avatar: "https://example.com/maya.jpg",
    stories: [
      { id: "m1", source: "https://example.com/s1.jpg", alt: "Mountain ridge", time: "2h", caption: "Made it to the top" },
      { id: "m2", source: "https://example.com/s2.jpg", alt: "Tents in the snow", time: "1h" },
    ],
  },
];

export function StoryTray() {
  const [open, setOpen] = useState(false);
  const [seen, setSeen] = useState<string[]>([]);
  const origin = useRef<ComponentRef<typeof View> | null>(null);
  const allSeen = users[0].stories.every((s) => seen.includes(s.id));
  return (
    <>
      <Avatar
        ref={origin}
        name={users[0].name}
        source={users[0].avatar}
        size="lg"
        story={allSeen ? "seen" : "unseen"}
        onPress={() => setOpen(true)}
      />
      <Stories
        users={users}
        open={open}
        onClose={() => setOpen(false)}
        seen={seen}
        onStoryView={(_, id) => setSeen((s) => [...s, id])}
        originRef={origin}
      />
    </>
  );
}
```

## Examples

### A text card instead of a photo

```tsx
{ id: "recap", content: <WeekCard km={42} />, duration: 4000, time: "5h" }
```

### Start at the person that was tapped

```tsx
<Stories users={users} open={open} initialUser={tapped} onClose={() => setOpen(false)} originRef={originRef} />
```

## API reference

### Stories

`import { Stories } from "@/components/goodui-native/stories/stories";`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `users` (required) | `StoryUser[]` | – | People with stories, in the order the viewer moves through them. |
| `open` (required) | `boolean` | – | Shows the viewer. It closes itself through `onClose`. |
| `onClose` (required) | `() => void` | – | Called when the person closes the viewer: the close button, a swipe down, Escape, or the end of the last story. |
| `initialUser` | `number` | `0` | Index into `users` to start with. Default 0. |
| `seen` | `string[]` | – | Ids of stories already seen. Each person starts at their first unseen story. |
| `onStoryView` | `(userId: string, storyId: string) => void` | – | Called when a story starts showing. Mark it seen here. |
| `originRef` | `RefObject<ComponentRef<typeof View> \| null>` | – | The view the viewer grows out of and returns into, for example the tapped Avatar. |
| `duration` | `number` | `5000` | Default time per story in milliseconds. Default 5000. |
| `haptic` | `boolean` | `true` | `false` silences the haptic on close. |
| `labels` | `Partial<StoriesLabels>` | – | Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide. |

## Accessibility

- The viewer is modal. The story is one adjustable element labelled 'Maya Patel, story 2 of 3' (plus the alt text) with the value 'Story 2 of 3': swipe up or down (increment, decrement) to step; the iOS escape gesture and Android's back button close.
- With a screen reader running (iOS and Android), stories do not advance on their own; each new story is announced with its caption. The web cannot detect a screen reader, so there the pause button and Space stop the timer.
- A pause and play button (aria-pressed on the web) and a close button sit in the header, 44 pt each, so moving content can always be stopped.
- Web keys: right and left arrows step (mirrored in right-to-left layouts), Space pauses, Escape closes.
- Captions sit on a dark overlay card; the header sits on a scrim, so white text keeps its contrast on bright photos.

## Motion

- Open: the viewer grows out of originRef's box (scale, position and a rounder corner) on the smooth spring while the backdrop fades in; without an origin it rises 40 pt from 0.94 scale.
- The current bar fills linearly over the story's duration on the UI thread, starting once a photo has loaded; finished bars are full, upcoming ones empty.
- Touch down pauses at once; after a 220 ms hold the bars, header and caption fade out, and they return on release.
- A sideways drag moves between people 1:1 (a plain slide, no cube) and settles on the smooth spring with the release velocity; the chrome fades while the pages are between people.
- Dragging down moves and shrinks the story and fades the backdrop; past 120 pt (or a fast flick) it closes back into the origin, otherwise it springs back.
- Photos fade in once loaded, over a spinner.
- Reduced motion: opening and closing fade, switching people is a cut, photos appear without fading. The bars still fill because they show time, and a drag still follows the finger.

## Haptics

- tap when the viewer closes from the close button, a swipe down, Escape or the end of the last story (haptic={false} turns it off)

## Performance

- Progress, pause and resume, swipes and the dismiss drag run as worklets; React re-renders when the story or person changes.
- Only the current person and the neighbours on each side are mounted, each showing one story.
- Images load as each story comes up; preload upcoming URLs with Image.prefetch if your network is slow.

## Notes for AI

- open is controlled: set it true to show, and set it false in onClose. Keep Stories mounted; it animates out before it unmounts its overlay.
- Every user needs at least one story. A story has either source (photo) or content (any view that fills it).
- Pass seen (story ids) so each person starts at their first unseen story, and add ids in onStoryView. Flip the matching Avatar's story prop to 'seen' when all of a person's stories are seen; the ring sweeps.
- originRef should point at the avatar the person tapped (Avatar takes a ref). Set it right before opening.
- Stories are always drawn on the dark palette (media chrome) regardless of the app's color scheme.
- Durations are in milliseconds per story (default 5000); text cards usually want less.

## Related

- [Avatar](https://goodui.dev/native/components/avatar.md): A photo that fades in over initials on a color picked from the name, with a presence dot and a story ring whose accent arc sweeps in when there is something new and away once seen; AvatarGroup stacks people with a +N counter and fans out on tap.
- [Image viewer](https://goodui.dev/native/components/image-viewer.md): Tap a thumbnail and the photo flies out of it into a full-screen viewer, its crop opening up as it lands; pinch or double-tap to zoom at your fingers, drag to pan, swipe between photos, and drag down to send it back into its thumbnail while the background fades. On the web the wheel zooms toward the pointer.
- [Carousel](https://goodui.dev/native/components/carousel.md): A snapping card carousel that follows the finger 1:1, shrinks and dims the neighbours, stretches its pagination pill with the drag, and turns that pill into a timer when it autoplays.

## Guidance for AI tools

Stories: An Instagram-style story viewer that grows out of the tapped avatar: segmented bars fill at the top, tap right or left to step, hold to pause while the chrome steps aside, swipe sideways to the next person with a plain slide, and drag down to send it back into the avatar. Use only the documented props. Keep accessibility, reduced motion and both color schemes intact when adapting it.

Full library index: https://goodui.dev/native/llms.txt
