Holo

Story Viewer

Component

About

A full-screen, story-style viewer: tap the edges to move between segments, press and hold to pause, swipe sideways to jump between stories, and swipe down to dismiss with the whole card shrinking and fading under your finger. Every frame gets a slow continuous zoom (a "Ken Burns" effect) that keeps running even while paused, so nothing on screen ever looks like a frozen screenshot.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Story Viewer to your project and fits it to your theme.

Code

How Story Viewer is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

import * as React from "react";
import { StoryViewer, type Story } from "@/components/ui/story-viewer";

// Small deterministic placeholder generators so demos never depend on the
// network. Everything is an inline SVG data URI with abstract shapes -
// no real photos, no brand marks, nothing identifiable.

function svgToDataUri(svg: string) {
  return `data:image/svg+xml,${encodeURIComponent(svg)}`;
}

function gradientPhoto(seed: number, w = 780, h = 1386) {
  const palettes = [
    ["#6366f1", "#a855f7", "#ec4899"],
    ["#0ea5e9", "#6366f1", "#8b5cf6"],
    ["#f59e0b", "#ef4444", "#ec4899"],
    ["#10b981", "#06b6d4", "#6366f1"],
    ["#f43f5e", "#f59e0b", "#eab308"],
    ["#8b5cf6", "#ec4899", "#f43f5e"],
  ];
  const [c1, c2, c3] = palettes[seed % palettes.length];
  const cx = 20 + ((seed * 37) % 60);
  const cy = 20 + ((seed * 53) % 60);
  const svg = `
  <svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
    <defs>
      <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
        <stop offset="0%" stop-color="${c1}"/>
        <stop offset="55%" stop-color="${c2}"/>
        <stop offset="100%" stop-color="${c3}"/>
      </linearGradient>
      <radialGradient id="r" cx="${cx}%" cy="${cy}%" r="70%">
        <stop offset="0%" stop-color="white" stop-opacity="0.35"/>
        <stop offset="100%" stop-color="white" stop-opacity="0"/>
      </radialGradient>
    </defs>
    <rect width="${w}" height="${h}" fill="url(#g)"/>
    <rect width="${w}" height="${h}" fill="url(#r)"/>
    <circle cx="${(cx / 100) * w}" cy="${(cy / 100) * h}" r="${w * 0.42}" fill="white" opacity="0.07"/>
  </svg>`;
  return svgToDataUri(svg);
}

function avatarPlaceholder(seed: number, size = 96) {
  const palettes = ["#6366f1", "#0ea5e9", "#f59e0b", "#10b981", "#f43f5e", "#8b5cf6"];
  const bg = palettes[seed % palettes.length];
  const letters = ["A", "J", "K", "M", "R", "S", "T", "W"];
  const letter = letters[seed % letters.length];
  const svg = `
  <svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
    <rect width="${size}" height="${size}" rx="${size}" fill="${bg}"/>
    <text x="50%" y="54%" text-anchor="middle" dominant-baseline="middle"
      font-family="system-ui, sans-serif" font-size="${size * 0.42}" fill="white" font-weight="600">${letter}</text>
  </svg>`;
  return svgToDataUri(svg);
}



const STORIES: Story[] = [
  {
    id: "s1",
    name: "field.notes",
    avatar: avatarPlaceholder(5),
    segments: [
      { id: "a", image: gradientPhoto(0), caption: "Sunrise over the ridge line.", durationMs: 4200 },
      { id: "b", image: gradientPhoto(1), caption: "Coffee at basecamp.", durationMs: 4200 },
      { id: "c", image: gradientPhoto(2), caption: "Last light before the descent.", durationMs: 4200 },
    ],
  },
  {
    id: "s2",
    name: "studio.log",
    avatar: avatarPlaceholder(2),
    segments: [
      { id: "d", image: gradientPhoto(3), caption: "New palette, same bones.", durationMs: 4200 },
      { id: "e", image: gradientPhoto(4), caption: "Draft three, finally right.", durationMs: 4200 },
    ],
  },
];

export default function StoryViewerDemo() {
  const [open, setOpen] = React.useState(true);

  if (!open) {
    return (
      <div className="flex h-full w-full items-center justify-center bg-zinc-950">
        <button
          onClick={() => setOpen(true)}
          className="rounded-full bg-white px-5 py-2.5 text-sm font-medium text-zinc-900"
        >
          Watch stories again
        </button>
      </div>
    );
  }

  return <StoryViewer stories={STORIES} onClose={() => setOpen(false)} />;
}

Dependencies

More by Holo

View all
  • Testimonial Coverflow

    Testimonial Coverflow

    4
  • Slot Week Picker

    Slot Week Picker

    2
  • Command Palette

    Command Palette

    2
  • Pricing Plan Tilt

    Pricing Plan Tilt

    1
  • File Drop Physics

    File Drop Physics

    1
  • Snap Sheet

    Snap Sheet

  • Reach Pin Pad

    Reach Pin Pad

  • Liquid Tab Bar

    Liquid Tab Bar