ScreenshotNeo

BlogHow-to

How to Create a Website Video Preview

Build a fast, accessible website video preview with a poster image, muted hover playback, lazy loading, captions, and reliable fallbacks.

By the ScreenshotNeo team1 October 20267 min read

Use a poster-first <video> element. Show a useful still image immediately, defer video downloads until intent, start muted inline playback only on hover or focus, and keep an explicit play control and text fallback. This approach remains usable when autoplay is blocked, motion is reduced, or the browser cannot play the source.

1. The complete poster-first implementation

This example supports a poster, WebM and MP4 sources, captions, keyboard focus, hover and focus previews, reduced-motion preferences, and an explicit play button.

<article class="preview-card">
  <a class="preview-link" href="/demo">Product demo</a>

  <div class="preview-frame">
    <video
      class="preview"
      muted
      playsinline
      preload="none"
      poster="/media/demo-poster.jpg"
      aria-describedby="demo-summary"
    >
      <source src="/media/demo.webm" type="video/webm">
      <source src="/media/demo.mp4" type="video/mp4">
      <track
        kind="captions"
        srclang="en"
        src="/media/demo.en.vtt"
        label="English"
      >
      <p id="demo-fallback">
        This browser cannot play the preview.
        <a href="/media/demo.mp4">Download the video</a>.
      </p>
    </video>

    <button class="preview-play" type="button" aria-label="Play product demo">
      Play
    </button>
  </div>

  <p id="demo-summary">A 12-second demonstration of the product workflow.</p>
</article>
.preview-card {
  max-width:  thirtyrem;
}

.preview-frame {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: #111;
}

.preview {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.preview-play {
  position: absolute;
  inset: auto 1rem 1rem auto;
  z-index: 1;
}

.preview-card:focus-within,
.preview-card:hover {
  outline: 3px solid #2563eb;
  outline-offset: 4px;
}

@media (prefers-reduced-motion: reduce) {
  .preview {
    transition: none;
  }
}

Replace thirtyrem with a valid CSS length such as 30rem.

const card = document.querySelector('.preview-card');
const video = card.querySelector('video');
const playButton = card.querySelector('.preview-play');
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');

async function startPreview() {
  if (reduceMotion.matches) return;

  try {
    video.currentTime = 0;
    await video.play();
    playButton.hidden = true;
  } catch {
    // Autoplay can be rejected. The poster and play button remain usable.
    playButton.hidden = false;
  }
}

function stopPreview() {
  video.pause();
  video.currentTime = 0;
  playButton.hidden = false;
}

card.addEventListener('mouseenter', startPreview);
card.addEventListener('focusin', startPreview);
card.addEventListener('mouseleave', stopPreview);
card.addEventListener('focusout', stopPreview);

playButton.addEventListener('click', async () => {
  try {
    if (video.paused) {
      await video.play();
      playButton.textContent = 'Pause';
      playButton.hidden = false;
    } else {
      video.pause();
      playButton.textContent = 'Play';
    }
  } catch {
    playButton.hidden = false;
  }
});

The poster is displayed while the video loads. The muted and playsinline attributes make hover playback eligible in more browsers, but script playback can still be rejected by browser policy or user settings. Always catch the promise returned by play(). See MDN’s video element reference.

2. Choose the preview trigger

Trigger Use it when Implementation
Explicit click The video carries important information or audio Keep the poster and play button; call video.play() from the click handler
Hover Desktop users need a quick visual scan Start on mouseenter, stop on mouseleave
Keyboard focus The card is reachable by keyboard Pair hover with focusin and focusout
IntersectionObserver You want previews near the viewport in a feed Load or play only while the card is sufficiently visible

Do not make hover the only way to discover or play a video. A visible link, button, or fallback paragraph gives keyboard and touch users an equivalent path.

3. Pick a loading strategy

Setting Effect Good default
preload="none" Expresses that no video data is needed before intent Large grids and carousels
preload="metadata" Allows metadata such as duration and dimensions to load Interfaces that display duration before play
preload="auto" Allows the browser to download more of the video Only when immediate playback is important
loading="lazy" Where supported, defers video and poster loading near the viewport Below-the-fold cards

Autoplay takes precedence over preload. Do not add autoplay="false": Boolean HTML attributes are enabled by their presence. Remove autoplay or control playback in JavaScript. See MDN’s preload documentation.

4. Generate and serve a good poster

  1. Export a representative frame that still makes sense without playback.
  2. Use the same aspect ratio as the video to avoid layout shifts and cropping.
  3. Serve an appropriately sized image instead of a full-resolution source for every card.
  4. Keep the poster available even when the video request is deferred or fails.
  5. Use a stable URL and a cache policy suitable for your deployment.

A poster is also the fallback for blocked autoplay, slow connections, unsupported codecs, and reduced-motion users. If you need a web page capture as a poster source, ScreenshotNeo can render a clean image of the page before you add your own video asset.

5. Add captions and alternatives

Use a WebVTT captions file for dialogue, meaningful sound effects, and other relevant audio:

WEBVTT

00:00.000 --> 00:03.000
Open the dashboard.

00:03.000 --> 00:07.500
Select a project and review the latest result.

00:07.500 --> 00:12.000
Export the report.

WCAG 2.2 requires captions for prerecorded synchronized media when the audio contains relevant information. Provide a text alternative or audio description when visual information is necessary. The W3C technique H95 documents the <track kind="captions"> pattern.

  • Keep the video and play control keyboard reachable.
  • Show a visible focus state.
  • Respect prefers-reduced-motion: reduce by suppressing nonessential hover playback.
  • For motion that starts automatically and lasts more than five seconds, provide pause, stop, or hide controls. See WCAG pause, stop, hide.

6. Lazy-start previews with IntersectionObserver

const cards = document.querySelectorAll('.preview-card');
const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    const video = entry.target.querySelector('video');
    if (!video) return;

    if (entry.isIntersecting) {
      // Keep the poster until the user focuses or hovers the card.
      entry.target.dataset.nearViewport = 'true';
    } else {
      video.pause();
      video.currentTime = 0;
    }
  });
}, { rootMargin: '200px 0px', threshold: 0.25 });

cards.forEach((card) => observer.observe(card));

This pattern prevents off-screen previews from continuing to play. Combine it with preload="none" when most cards are never opened.

7. Browser and media compatibility

  • Provide at least WebM and MP4 sources with accurate MIME types.
  • Keep the fallback paragraph inside <video> for browsers that cannot play the element.
  • Use playsinline when the preview should stay in the page on mobile browsers.
  • Do not rely on audible autoplay. Modern browsers commonly block videos with an unmuted audio track.
  • Test the poster, play button, captions, source selection, and fallback independently.

8. Performance, reliability, and delivery cost

  • Keep preview clips short and compress them for their display size.
  • Use preload="none" for large collections and metadata only when the interface needs duration or dimensions.
  • Use lazy loading for below-the-fold cards where the target browsers support it.
  • Serve media through infrastructure that can handle the expected bandwidth and concurrent range requests; measure your own traffic rather than relying on a universal file-size target.
  • Stop playback when a card leaves the viewport or loses focus.
  • Log rejected play() promises and media errors so you can distinguish policy blocks from missing files, CORS problems, and codec failures.
  • Cache immutable poster and video URLs, and version filenames when replacing assets.

9. Troubleshooting

Symptom Likely cause Fix
Hover playback does nothing play() was rejected or the video has audible audio Keep muted, catch the promise, and retain an explicit play button
The video downloads on every page load preload="auto" or autoplay is enabled Use preload="none" and start playback only after intent
The poster is missing Bad URL, server error, unsupported format, or lazy-loading timing Open the poster URL directly, check response headers, and keep a CSS background or fallback visual if needed
Video is cropped unexpectedly object-fit: cover or mismatched aspect ratios Use a matching poster ratio or switch to contain
Captions do not appear Incorrect VTT syntax, path, language, or track mode Validate the VTT file, use kind="captions", and select the track in browser controls
Mobile opens a fullscreen player Inline playback was not requested Add playsinline and test on the target mobile browsers
Cards consume too much bandwidth Every card preloads or continues playing off-screen Use preload="none", lazy loading, IntersectionObserver, and short clips
Autoplay starts despite autoplay="false" Boolean attributes are enabled by presence Remove the attribute entirely

10. Or skip the browser setup

For a clean page image you can use as a poster source, ScreenshotNeo accepts one GET request and returns PNG, JPEG, WebP, or PDF. Its consent step accepts cookie banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. The service also provides an MCP server for AI agents through take_screenshot, get_page_info, and capture_pdf.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

See the ScreenshotNeo API documentation for all capture options. Free accounts include 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots, and every feature is included on every plan. Create a free ScreenshotNeo account.

11. FAQ

Should a preview autoplay with sound?

No. Use muted inline playback for an optional visual preview, and require a click when sound or important information is involved.

Is a poster required?

It is the most reliable first state and fallback. Use one even when the video normally starts quickly.

Which preload value should I choose?

Start with none for grids, use metadata when you need duration or dimensions, and reserve auto for previews where early loading is intentional.

How do I support users who do not want animation?

Check prefers-reduced-motion: reduce, skip hover playback, and leave the poster and an explicit play control available.

Can a preview replace the full video page?

No. Link the card to a page or player with complete controls, captions, transcript or equivalent alternatives, and the full experience.