DEV Community

Rails Designer
Rails Designer

Posted on • Originally published at railsdesigner.com

Video Preview on Hover with Stimulus

This article was originally published on Rails Designer's blog


Building on the video recording feature from earlier, let's add a nice touch to the presentation index page: video previews that play on hover. You know the experience—hover over a video thumbnail and get a quick preview of what's inside. It's the same interaction you see on YouTube, Netflix and every modern video platform.

The presentations index

After recording presentations, you need a way to browse them. A simple index page lists all presentations with video thumbnails. When you hover over a thumbnail, the video plays a preview. Move your cursor away and it returns to the poster image.

Here's the basic setup:

<% @presentations.each do |presentation| %>
  <%= video_tag presentation.video,
    width: "160",
    poster: (presentation.video.representable? ? url_for(presentation.video.representation(resize: "160x120")) : nil),
    data: {
      controller: "preview",
      action: "mouseenter->preview#play mouseleave->preview#pause"
    }
  %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

Active Storage's representable? method checks if a preview can be generated and representation() creates the thumbnail automatically.

The preview controller

All the logic happens in one Stimulus controller:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = {
    segments: { type: Number, default: 3 },
    interval: { type: Number, default: 1000 },
    minDuration: { type: Number, default: 5 }
  }

  connect() {
    this.originalTime = 0
    this.wasPlaying = false
    this.previewTimer = null
    this.currentIndex = 0
    this.isReady = false
    this.timestamps = []

    this.element.addEventListener("loadedmetadata", () => {
      this.#calculateTimestamps()

      this.isReady = true
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

Understanding loadedmetadata

The loadedmetadata event is the key here. It fires when the browser has loaded enough of the video to know its duration, dimensions and other metadata. Without this information, you can't calculate meaningful preview timestamps.

this.element.addEventListener("loadedmetadata", () => {
  this.#calculateTimestamps()
  this.isReady = true
})
Enter fullscreen mode Exit fullscreen mode

Only after loadedmetadata fires can you access this.element.duration reliably. Try to use it before this event and you'll get NaN or 0.

Smart timestamping

Instead of just playing from the beginning, the controller shows different parts of the video:

#calculateTimestamps() {
  const duration = this.element.duration

  if (duration < this.minDurationValue) {
    this.timestamps = [0]
    return
  }

  this.timestamps = []
  for (let i = 1; i <= this.segmentsValue; i++) {
    this.timestamps.push((duration / (this.segmentsValue + 1)) * i)
  }
}
Enter fullscreen mode Exit fullscreen mode

For short videos (under 5 seconds), it just plays from the start. For longer videos, it divides the duration into segments and cycles through them. A 60-second video with 3 segments shows clips at 15, 30 and 45 seconds.

The element methods

Stimulus gives you direct access to the video element through this.element. Here are the key methods and properties used:

  • this.element.duration for the total video length in seconds
  • this.element.currentTime for current playback position
  • this.element.paused is a boolean indicating if video is paused
  • this.element.play() to start playback
  • this.element.pause() to pause playback
  • this.element.muted is a boolean to control audio during preview

The preview controller leverages these to create smooth hover interactions:

play() {
  if (!this.isReady || this.timestamps.length === 0) return

  this.originalTime = this.element.currentTime
  this.wasPlaying = !this.element.paused
  this.currentIndex = 0

  this.element.muted = true
  this.#showNextTimestamp()

  if (this.timestamps.length > 1) {
    this.previewTimer = setInterval(() => {
      this.#showNextTimestamp()
    }, this.intervalValue)
  }
}
Enter fullscreen mode Exit fullscreen mode

When you hover, it saves the current state, mutes the audio and starts cycling through preview segments. When you leave, it restores everything exactly as it was.

Auto-looping previews

The preview automatically loops through different parts of the video:

#showNextTimestamp() {
  this.element.currentTime = this.timestamps[this.currentIndex]
  this.element.play()

  this.currentIndex = (this.currentIndex + 1) % this.timestamps.length
}
Enter fullscreen mode Exit fullscreen mode

The modulo operator (%) creates the loop: when it reaches the last segment, it goes back to the first. Combined with setInterval, this creates a cycling preview that gives your users a real sense of the video content.


The key is waiting for the right moment (when metadata loads), calculating smart preview points and cleaning up properly when the interaction ends. This preview controller shows how a small Stimulus controller can create a nice UX. By understanding browser events like loadedmetadata and leveraging the video element's built-in methods, you can get a lot done without writing a lot of JavaScript. 🥳

Top comments (0)