The production symptom
The bug report was short, which is usually a gift: the hero video was changing slides before the video duration had fully finished. The expected behavior was equally clear: when the current slide is a video, the carousel should move only after that video has completed.
This kind of bug is easy to misdiagnose because many commerce storefronts have more than one slider. A product gallery, a bundle drawer slider, and a homepage hero carousel can all use similar words in filenames. The important first step is to find the component that owns the actual behavior. In this case, the fix belonged in the home hero carousel, not in a product gallery.
The underlying issue was the common carousel shortcut: a global autoplay interval advanced every few seconds. That works for static image slides. It is wrong for video slides because the interval knows nothing about the active video duration.
Why a fixed timer is the wrong owner
A hero carousel with mixed media has two different timing rules.
| Slide type | Better advancement rule |
|---|
| Image slide | Use the normal timed autoplay interval |
| Video slide | Wait for the active video to emit its completion event |
| Inactive video slide | Ignore its media events |
| Last slide | Only advance if the carousel should continue or loop |
The mistake is treating video as a longer image. A five-second interval is not a video lifecycle. If the video is eight seconds long, the user loses three seconds. If the video is two seconds long, the carousel feels stuck. The media element already knows when playback has ended, so the carousel should listen to that signal instead of guessing.
This is also why the fix should stay close to the hero components. Changing product media behavior would not fix a homepage hero bug. It would only create risk in a different surface.
The implementation shape
The clean fix is to split autoplay responsibility by active slide type.
- Keep the existing timed autoplay for image slides.
- Detect when the selected hero slide is a video.
- Do not run the fixed interval while a video slide is active.
- Pass an isActive flag and an onVideoEnded callback into the rendered slide.
- Attach the callback to the video element's native onEnded handler.
- Guard the handler so only the active slide can advance the carousel.
A simplified React shape looks like this:
const isActiveVideo = activeSlide.type === "video";
useEffect(() => {
if (isActiveVideo) return;
const timer = window.setInterval(() => emblaApi?.scrollNext(), 5000);
return () => window.clearInterval(timer);
}, [emblaApi, isActiveVideo, activeIndex]);
function handleVideoEnded(slideIndex: number) {
if (slideIndex !== activeIndex) return;
emblaApi?.scrollNext();
}
<HeroSlide
slide={slide}
isActive={index === activeIndex}
onVideoEnded={() => handleVideoEnded(index)}
/>
And inside the slide component, the video should avoid looping if the carousel depends on the ended event:
<video
autoPlay={isActive}
muted
playsInline
loop={false}
onEnded={isActive ? onVideoEnded : undefined}
>
<source src={slide.videoUrl} type="video/mp4" />
</video>
The exact names should match the project. The point is the ownership boundary: the carousel manages slide movement, but the active video decides when its own playback is complete.
The guard that prevents subtle bugs
The active-slide guard matters more than it looks. Carousels often keep neighboring slides mounted for smooth movement. A video element can finish, reset, or fire an event while it is no longer the selected slide. If that stale event calls scrollNext, the carousel can jump unexpectedly.
The handler should therefore ask a boring but important question before doing anything: is this still the active slide? If the answer is no, return immediately.
That one guard keeps the fix from turning into a new timing bug.
What should not change
This fix should not rewrite carousel styling, image behavior, product gallery behavior, or Shopify API code. The issue is not a Storefront API problem and it does not require a GraphQL query change.
If the storefront renders Shopify product media through Hydrogen's video media component, that is a separate rendering concern. The timing rule still stays the same: do not let a generic interval advance a video slide before the active media has completed.
Keeping the scope small is part of the quality of the fix. A homepage hero timing bug should not become a product gallery refactor.
Verification checklist
The useful checks are practical:
- Start on an image slide and confirm the existing timed autoplay still works.
- Start on a video slide and confirm the carousel does not move at the old fixed interval.
- Let the video play fully and confirm the slide changes only after completion.
- Move away from a video manually and confirm the old video cannot advance the carousel later.
- Confirm the last-slide behavior still matches the carousel's loop setting.
- Run TypeScript and lint checks to make sure the callback props and guards remain strict.
For this class of bug, a browser check is worth more than just reading the diff. The fix is about time, state, and media events. You want to see the video reach the end before the carousel moves.
The lesson
The practical lesson is not only "use onEnded." It is to let the right owner control the transition.
Image slides can be timer-driven because they have no natural completion event. Video slides do have one. When the active slide is a video, the browser's media lifecycle is more accurate than a guessed interval.
That small distinction makes the hero feel intentional instead of rushed, and it keeps the storefront behavior aligned with what the shopper actually sees.
If your Hydrogen storefront has small media bugs like this, the useful work is not only patching one carousel. It is checking whether the component ownership, timers, and browser events still match the buying experience you intended.