Stories Tray
The row of rings at the top of a feed: one per person, bright while they have something new, muted once the viewer has seen it all. Pressing a ring opens the stories at the first one not yet seen, and the next visit, even after a reload, remembers where the viewer got to.
Try it
Every version imports ./api, which stands in for your backend: groups is the people a viewer follows with the stories each posted, in the shape of the player's StoriesGroup. Replace it with your own feed.
import { cdnUrl } from '@reelkit/example-data';
// Stands in for your backend: the people a viewer follows and the stories each
// one posted. Each entry has the shape of `StoriesGroup` from the stories
// player of your framework, so the list goes to the player as it is.
export interface Group {
author: { id: string; name: string; avatar: string };
stories: { id: string; mediaType: 'image'; src: string }[];
}
const kPeople = ['Alex', 'Sarah', 'Mike', 'Emma', 'Noah', 'Lena'];
// Uneven on purpose: a person with one story is watched to the end at once,
// one with three can be left part way and resumed.
const kStoryCounts = [3, 1, 2, 3, 1, 2];
const pad = (n: number) => String(n).padStart(2, '0');
const storyImage = (n: number) =>
n <= 6
? cdnUrl(`samples/images/stories/story-00${n}.jpg`)
: cdnUrl(`samples/images/image-${pad(n - 6)}.jpg`);
let nextImage = 1;
export const groups: Group[] = kPeople.map((name, i) => ({
author: {
id: `user-${i + 1}`,
name,
avatar: cdnUrl(`samples/avatars/avatar-${pad(i + 1)}.jpg`),
},
stories: Array.from({ length: kStoryCounts[i] }, (_, j) => ({
id: `user-${i + 1}-story-${j + 1}`,
mediaType: 'image' as const,
src: storyImage(nextImage++),
})),
}));The sample media works only on reelkit.dev
cdnUrl builds links to cdn.reelkit.dev, which serves its files only to the ReelKit sites. In your app those avatars and stories fail to load, so point avatar and src at your own files before you copy this file.
Watch someone to the end and close the player: their ring turns muted, focus lands on the last person you watched, and both survive a reload. "Forget what was seen" starts over.
Why it works
- One controller remembers for both.
createStoriesViewedStateControlleris created once and goes to the ring list and the player asviewed. The rings read from it, the player records every story it shows into it, and it keeps that inlocalStorageunderstorageKey, so a reload changes nothing. See remembering what was seen for React, Vue, and Angular. - The player picks up where the viewer left off. Given
viewed, the player opens a person on their first story not yet seen, or from the start once everything was seen. The recipe only passes the person, never a story index; the controller'sresumeStoryIndexanswers that, see its reference. - A ring shows two states. Bright while there is something left to watch, muted once a person was watched to the end. Progress inside a person is the player's progress bar, not the ring. See StoriesRing.
- The ring colours follow the page. The two states are the tokens
--rk-stories-ring-active-gradientand--rk-stories-ring-viewed-color, and the watched one defaults to a faint white made for a dark page. On a light page set it on.rk-stories-ring-list, as this demo does in the light theme, not on:root: the player's desktop carousel draws rings with the same tokens on its dark backdrop. The tokens are in the theming table. - No extra wiring for the controller. The player is always rendered and opened with
isOpen, so it reads the stored state itself before it chooses the first story. Mount it only while it is open and the page has to attach the controller first, as the controller reference explains. - Focus comes back to the right ring. As it closes, the player returns focus to the ring that opened it, as its accessibility section describes. That happens after the close handler has run, so the recipe waits one task, then scrolls the ring of the last person watched into view and focuses it.
- Where else it can be kept.
storagetakes your own adapter, a server for instance, so what was seen follows the viewer across devices.ttlMslets a person's entry expire, andmaxTrackscaps how many people are kept. They are in the controller config.
Next Steps
- Infinite Feed - load more as the viewer nears the end of a feed
- Stories Player - every prop, slot, and part of the playerStories Player - every prop, slot, and part of the playerStories Player - every prop, slot, and part of the player