javascripthtmlcssscrollhover

How to Create a Horizontally Scrollable Element with Hover Overlay Effect in HTML/CSS?


I’m working on a webpage where I need to create an element with the following properties:

Here’s my current HTML structure:

<div class="horizontal-scroll-videos">
    {% for dance in dances %}
        <a href="/sample/{{ dance.id }}" class="dance-card">
            <div class="dance-card-image-description">
                <img src="{{ dance.image_url }}" class="dance-card-image" alt="{{ dance.name }}">
                <video class="dance-card-video" dance-video-path="{{ dance.video }}" muted></video>
                <p class="dance-card-description body-text">{{ dance.description }}</p>
            </div>
            <div class="dance-card-body">
                <h5 class="dance-card-title">{{ dance.name }}</h5>
                <div class="dance-card-tags">
                    <span class="badge bg-primary">{{ dance.difficulty }}</span>
                    <span class="badge bg-secondary">{{ dance.genre }}</span> 
                    {% if dance.has_instructions %}
                    <span class="badge bg-warning">Tutorial</span> <!-
                    {% endif %}
                </div>
            </div>
        </a>
    {% endfor %}
</div>

I have managed to implement each of these features individually; but problem is that I cannot find a way to have both behaviours functioning at the same time.

I've found out that the problem is around the definition of the horizontal-scroll-videos element, particularly about the overflow-x.

.horizontal-scroll-videos {
  display: flex;
  align-items: flex-start;
  gap: var(--inter-video-gap);
  align-self: stretch;
  overflow-x: auto;
  overflow-y: visible;
  scroll-behavior: smooth;
  white-space: nowrap;
  scrollbar-width: none;
}

When overflow-x: auto is enabled: The horizontal scrolling works perfectly. However, the hover effect is cut off, and the dance cards are not able to overlay other elements as desired.

Srolling working but hover not working

When overflow-x: visible is enabled: The hover effect works correctly, and the dance cards overlay other elements as desired. However, the horizontal scrolling functionality is lost.

Hover working but scrolling not working

I have tried to dynamically toggle overflow-x behavior based on user interaction, but the problem is that the scroll loses its position. When switching back to overflow-x: visible, the scroll position resets, and the new content displayed during the scroll is lost.

Code for the hover effect and the scrolling effect is:

function addHoverEffectDanceCards(){
    const danceCards = document.querySelectorAll(".dance-card");
    danceCards.forEach(danceCard => {
        // Get the video and image elements within the dance card
        const video = danceCard.querySelector(".dance-card-video");
        const image = danceCard.querySelector(".dance-card-image");

        // Get the children (the elements within) the dance card
        const children = danceCard.children;
        const childrenArray = Array.from(children);

        childrenArray.forEach(child => {
            // If any element in a dance card gets moused over, add the hover class to every element in the dance card
            child.addEventListener("mouseover", function() {
                const container = danceCard.closest(".horizontal-scroll-videos");
                const containerRect = container.getBoundingClientRect();
                const danceCardRect = danceCard.getBoundingClientRect();
                // Check if the dance card is fully visible within the container; don't show preview if it is not
                if (danceCardRect.left >= containerRect.left && 
                    danceCardRect.right <= containerRect.right) {
                    childrenArray.forEach(child => {
                        classes = child.classList;
                        classes.add("hover");

                        // Add the hover to the children within the dance-card-image-description div
                        if (classes.contains("dance-card-image-description")) {
                            const imgDesChildren = child.children;
                            const imgDesChildrenArray = Array.from(imgDesChildren);

                            imgDesChildrenArray.forEach(imgDesChild => {
                                imgDesChild.classList.add("hover");
                            });
                        };
                    });

                    // Add the hover class to the dance card itself
                    danceCard.classList.add("hover");

                    // Check if the video src for preview is loaded
                    if (!video.src) {
                        // Get the video if it is not defined
                        fetch('/generate_video_url', {
                            method: 'POST',
                            headers: {
                                'Content-Type': 'application/json'
                            },
                            body: JSON.stringify({video_path: video.getAttribute('dance-video-path')})
                        })
                        .then(response => response.json())
                        .then(data => {
                            video.src = data.video_url; // Asign the video
                        })
                        .catch(error => console.error('Error fetching video presigned URL:', error));
                    } 
                    
                    // Start playing the preview
                    video.currentTime = 0;
                    video.play();
                    video.style.display = "block";
                    image.style.display = "none";
                }
            });

            // Remove the hover when no longer mousing over
            child.addEventListener("mouseout", function() {
                childrenArray.forEach(child => {
                    classes = child.classList;
                    classes.remove("hover");

                    // Remove the hover effect from the children inside the dance-card-image-description div
                    if (classes.contains("dance-card-image-description")) {
                        const imgDesChildren = child.children;
                        const imgDesChildrenArray = Array.from(imgDesChildren);
        
                        imgDesChildrenArray.forEach(imgDesChild => {
                            imgDesChild.classList.remove("hover");
                        });
                    };
                });

                // Remove the hover class from the dance card itself
                danceCard.classList.remove("hover");

                // Pause the video and show the image
                video.pause();
                video.style.display = "none";
                image.style.display = "block";
            });
        });
    });

const horizontalScrollContainers = document.querySelectorAll(".horizontal-scroll-videos");

horizontalScrollContainers.forEach(container => {
    let scrollInterval;

    container.addEventListener('mouseover', (e) => {
        const screenWidth = window.innerWidth;
        const scrollThreshold = 200;
        // Check if mouse is within the scrollThreshold from the right edge
        const checkLeftScroll = (e) => {
            const mouseX = e.clientX;
            return mouseX > screenWidth - scrollThreshold;
        };
        const checkRightScroll = (e) => {
            const mouseX = e.clientX;
            return mouseX < scrollThreshold;
        }
        if (checkLeftScroll(e)) {
            scrollInterval = setInterval(() => {container.scrollLeft += 180;}, 30);
        } else if (checkRightScroll(e)) {
            scrollInterval = setInterval(() => {container.scrollLeft -= 180;}, 30);
        }
    });

    container.addEventListener('mouseout', () => {
        clearInterval(scrollInterval);
        scrollInterval = null;
    });
});

I am a beginner in this field, and any help or suggestions on how to achieve this would be greatly appreciated!


Solution

  • Edit

    It seems that I completely misinterpreted your question. Unfortunately overflow-x and overflow-y have the default unsavory behavior of not being able to have one being set to visible and the other to scroll/hidden. Here is a Stack Overflow question that explains it in detail.

    I would suggest using one of the solutions they present.

    Original answer for those trying to solve :hover and mouse-move problems

    The reason the hover effect stops working is because hover effect only checks when you move the mouse over an element, NOT if that element moves under the mouse while the mouse isn't moving.

    Because your scrolling is automatic, when the dance cards move under the mouse :hover is not triggered.

    To preserve the functionality of both, you would probably need to check if the mouse is over the dance card with Javascript instead of CSS. I believe the Javascript events onmouseover and onmouseenter will have the same problems as CSS :hover.

    This makes things a little more difficult. One way you could approach this is by continually checking with document.elementFromPoint to get the element you are hovering over and if it is a dance card then trigger the effects you would with :hover. Then whenever the element is not a dance card, reset the value for that card. https://developer.mozilla.org/en-US/docs/Web/API/Document/elementFromPoint

    There may be simpler solutions to the problem but I think this is one way you could go about it.

    Here is another Stack Overflow question illustrating the same problem. You could try their solution as well: Triggering :hover on moving element without moving mouse