Skip to content
VistaView

Image Story Extension

The Image Story extension allows you to display rich HTML content (stories, descriptions, captions) below images in the lightbox. Content is loaded on-demand and cached for performance.

import { vistaView } from 'vistaview';
import { imageStory } from 'vistaview/extensions/image-story';
import 'vistaview/style.css';
import 'vistaview/styles/extensions/image-story.css';

vistaView({
  elements: '#gallery > a',
  extensions: [
    imageStory({
      getStory: async (index) => {
        const response = await fetch(`/api/stories/${index}`);
        const data = await response.json();
        return { content: data.html };
      },
    }),
  ],
});
<link rel="stylesheet" href="https://unpkg.com/vistaview/main/dist/style.css" />
<link rel="stylesheet" href="https://unpkg.com/vistaview/extensions/image-story/dist/style.css" />
<script src="https://unpkg.com/vistaview/main/dist/vistaview.umd.js"></script>
<script src="https://unpkg.com/vistaview/extensions/image-story/dist/main.umd.cjs"></script>

<script>
  VistaView.vistaView({
    elements: '#gallery > a',
    extensions: [
      VistaView.imageStory({
        getStory: async (index) => {
          const response = await fetch(`/api/stories/${index}`);
          const data = await response.json();
          return { content: data.html };
        },
      }),
    ],
  });
</script>

A function that returns a promise resolving to a story object:

getStory: (imageIndex: number) => Promise<StoryResult>;

StoryResult interface:

interface StoryResult {
  content: string; // HTML content (will be sanitized)
  onLoad?: () => void; // Called when story is displayed
  onUnload?: () => void; // Called when navigating away
}

Maximum number of stories to keep in memory. Default: 5

imageStory({
  getStory: async (index) => {
    /* ... */
  },
  maxStoryCache: 10, // Keep 10 stories cached
});
  • Rich HTML content - Display formatted text, images, links, etc.
  • On-demand loading - Stories are fetched only when needed
  • Smart caching - Recently viewed stories are cached
  • Auto-sanitization - HTML is automatically sanitized using DOMPurify
  • Expandable panel - Users can expand/collapse the story
  • Lifecycle hooks - onLoad/onUnload for custom behavior
import { imageStory } from 'vistaview/extensions/image-story';

vistaView({
  elements: '#gallery > a',
  extensions: [
    imageStory({
      getStory: async (index) => ({
        content: `
          <h2>Image ${index + 1}</h2>
          <p>This is the story for image ${index + 1}.</p>
        `,
      }),
    }),
  ],
});
imageStory({
  getStory: async (index) => {
    try {
      const response = await fetch(`/api/stories/${index}`);
      const data = await response.json();

      return {
        content: `
          <h2>${data.title}</h2>
          <p>${data.description}</p>
          <p><small>By ${data.author} on ${data.date}</small></p>
        `,
      };
    } catch (error) {
      return {
        content: '<p>Story could not be loaded.</p>',
      };
    }
  },
});
imageStory({
  getStory: async (index) => {
    const response = await fetch(`/api/stories/${index}`);
    const data = await response.json();

    return {
      content: data.html,
      onLoad: () => {
        console.log(`Story ${index} loaded`);
        // Track analytics
        gtag('event', 'story_view', { story_id: index });
      },
      onUnload: () => {
        console.log(`Story ${index} unloaded`);
        // Cleanup any event listeners
      },
    };
  },
});
const stories = [
  {
    title: 'Sunset at the Beach',
    content: 'A beautiful evening captured at the coast...',
  },
  {
    title: 'Mountain Peak',
    content: 'Hiking adventure to the summit...',
  },
];

imageStory({
  getStory: async (index) => ({
    content: `
      <h2>${stories[index].title}</h2>
      <p>${stories[index].content}</p>
    `,
  }),
});

The extension includes default styles, but you can customize:

/* Story container */
.vvw-story {
  /* Positioned at bottom of lightbox */
}

/* Story text area */
.vvw-story-text {
  background: rgba(0, 0, 0, 0.8);
  backdrop-filter: blur(10px);
  color: white;
  padding: 16px;
  border-radius: 8px;
}

/* Expanded state */
.vvw-story-text.expanded {
  max-height: 400px;
  overflow-y: auto;
}

/* Expand/collapse button */
.vvw-story-button {
  background: rgba(255, 255, 255, 0.1);
  border: none;
  color: white;
  cursor: pointer;
}

The extension automatically sanitizes HTML content using DOMPurify to prevent XSS attacks. However, you should still:

  1. Validate input on your server
  2. Use Content Security Policy (CSP) headers
  3. Sanitize user-generated content before storing

Stories are cached in memory to avoid redundant API calls:

imageStory({
  getStory: async (index) => {
    // This is only called once per image (until cache eviction)
    return await fetchStory(index);
  },
  maxStoryCache: 10, // Adjust based on memory constraints
});

Stories are only fetched when the user navigates to an image, not during initialization.

When the cache exceeds maxStoryCache, the oldest entries are evicted (FIFO).

  • ESM: 33.60 KB (10.84 KB gzip)
  • UMD: 25.28 KB (9.81 KB gzip)
  • CSS: 2.52 KB (0.72 KB gzip)

Note: Includes DOMPurify for HTML sanitization.

© 2026 • MIT License