A client-side Instagram-style Stories feature built with React. Users can upload images that appear as ephemeral stories, auto-expire after 24 hours, and are viewable in a fullscreen player with progress bars and swipe navigation.
- Add stories — tap the
+button to pick any image from your device - Auto-resize — images are scaled down to a maximum of 1080×1920px on a canvas before being stored, keeping localStorage usage lean
- 24-hour expiry — stories are timestamped on creation and automatically pruned when they exceed 24 hours old
- Fullscreen viewer — tap any story ring to open the viewer
- Segmented progress bars — one bar per story at the top of the viewer; the active bar fills over 3 seconds
- Auto-advance — moves to the next story when the timer completes, and closes the viewer after the last one
- Tap to navigate — tap the left half of the screen to go back, right half to go forward
- Swipe to navigate — swipe left or right to move between stories; holding pauses the timer
- Persistent storage — stories survive page refreshes via
localStorage - Responsive — works on both mobile and desktop
stories/
├── App.jsx # Root component — state, story management, layout
├── App.css # Global resets, CSS variables, layout, feed skeleton
├── components/
│ ├── StoryViewer.jsx # Fullscreen overlay with rAF progress loop and gestures
│ ├── StoryViewer.css # Viewer-specific styles
│ ├── StoryRing.jsx # Gradient ring thumbnail button
│ ├── AddButton.jsx # File picker trigger with image processing
│ └── TrayItems.css # Shared tray button and circle styles
└── utils/
├── image-utils.js # resizeImage(), formatAge()
└── storage-utils.js # loadStories(), saveStories(), createStory(), pruneExpiredStories()
- Node.js 18+
- A React project scaffolded with Vite or Create React App
-
Copy the
stories/folder into your project'ssrc/directory. -
Install dependencies (the project has none beyond React itself):
npm install- Mount
App.jsxas your root component inmain.jsx:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./stories/App.jsx";
createRoot(document.getElementById("root")).render(
<StrictMode>
<App />
</StrictMode>
);- Start the dev server:
npm run devWhen a user selects a file, AddButton passes it to resizeImage() in image-utils.js. The function creates an off-screen <canvas>, calculates the scale ratio needed to fit within 1080×1920px without upscaling, draws the image, and returns a base64 JPEG data URL at 85% quality.
Stories are saved to localStorage under the key stories_v1 as a JSON array. Each story object has the shape:
{ id: number, src: string, createdAt: number }loadStories() filters out any entries where createdAt is older than 24 hours before returning them, so expired stories are never rendered. App.jsx also runs pruneExpiredStories() on a 60-second interval to keep storage clean during long sessions.
StoryViewer drives progress with requestAnimationFrame rather than setInterval, giving smooth sub-frame accuracy. A startRef stores the timestamp when the current story began. On each frame, elapsed time is divided by the 3000ms duration to produce a 0–1 progress value used to set the active bar's width. Touching the screen sets a pausedRef flag that freezes startRef, effectively pausing the clock without cancelling the loop.
Constants you may want to adjust:
| Location | Constant | Default | Description |
|---|---|---|---|
utils/image-utils.js |
MAX_W |
1080 |
Max image width in px |
utils/image-utils.js |
MAX_H |
1920 |
Max image height in px |
components/StoryViewer.jsx |
STORY_DURATION |
3000 |
Ms each story is displayed |
utils/storage-utils.js |
TTL_MS |
86400000 |
Story lifetime in ms (24h) |
utils/storage-utils.js |
STORAGE_KEY |
"stories_v1" |
localStorage key |
App.jsx |
PRUNE_INTERVAL_MS |
60000 |
How often to prune expired stories |
Requires support for:
localStoragerequestAnimationFrameHTMLCanvasElement.toDataURLURL.createObjectURL- CSS
dvhunits (falls back gracefully in older browsers)
All modern browsers (Chrome, Firefox, Safari, Edge) are supported. The touch swipe gestures work on any touch-enabled device.
- Storage quota —
localStorageis limited to ~5MB per origin. High-resolution images encoded as base64 are roughly 33% larger than their original file size. Users adding many large images may hit this limit. A future improvement could compress more aggressively or use theStorageManagerAPI to check available quota before saving. - No multi-user support — stories are local to the browser and user. There is no backend or sync layer.
- Single author — all stories belong to the same local user. Extending to multiple user profiles would require additional data modelling in
storage-utils.js.