Video Preview on Hover with Stimulus
Source: Dev.to
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.
preview#play mouseleave->preview#pause"
}
%>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
})
}
}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
})Only after loadedmetadata fires can you access this.element.duration reliably. Trying to use it before this event will return 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 1) {
this.previewTimer = setInterval(() => {
this.#showNextTimestamp()
}, this.intervalValue)
}
}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 the calculated timestamps:
#showNextTimestamp() {
this.element.currentTime = this.timestamps[this.currentIndex]
this.element.play()
this.currentIndex = (this.currentIndex + 1) % this.timestamps.length
}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 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 small Stimulus controller demonstrates how a few lines of JavaScript can deliver a polished UX. 🥳