[JavaScript]特定の要素をHoverするとツールチップを表示する

特定の要素をHoverするとツールチップを表示する。
要件は下記

  • hoverタイミングでツールチップを表示
  • 特定の要素の上中央に表示する
  • 200ms遅延させて表示
  • hoverが終わるとツールチップを非表示

完成イメージはこんなかんじ。

目次

HTMLを定義する

今回は特定の要素をhoverした時にツールチップを表示するため、表示したいDOMに対してdata属性を追加します。

ツールチップなので「data-tooltip」としました。
このdata-tooltip属性でツールチップを表示したいテキストを設定します。

HTMLはこんな感じになります。

    <div style="width: fit-content; margin-top: 80px;">
        <div data-tooltip="ツールチップ">ホバーすると表示される</div>
    </div>

CSSを定義する

ツールチップを装飾します。

    .tooltip {
        position: absolute;
        background: #4b4848e0;
        color: #FFFFFF;
        font-size: 14px;
        line-height: 20px;
        padding: 8px 12px;
        border-radius: 4px;
        opacity: 0;
        transition: opacity 0.2s;
        pointer-events: none;
        transform: translateY(-8px);
        max-width: 184px;
    }

    .tooltip.show {
        opacity: 1;
    }

JavaScriptでtoolTipを実装する

DOMが読み込まれてから、data-tooltip属性をもつDOM一覧を取得し、それぞれのDOMにイベントをつけていきます。

    document.addEventListener('DOMContentLoaded', () => {
        const tooltipContainers = document.querySelectorAll('[data-tooltip]');

        tooltipContainers.forEach(container => {
            let tooltipTimeout;
            let tooltipElement;

            container.addEventListener('mouseenter', () => {
                // show tooltip
            });

            container.addEventListener('mouseleave', () => {
                 // hide tooltip
            });
        });
    });

mouseenterのイベントを書いていきます。

mouseenterの時は、setTimeoutで200msまったあとに、ホバー処理を書いていきます。
createElementでツールチップ用のDOMを生成し、ホバーした要素containerからツールチップの表示位置を計算し、ツールチップとして表示するtooltipElementのstyleを設定します。

DOMの位置によってはズレるので、必要に応じてwindow.scrollYなどを計算に加えます。

            container.addEventListener('mouseenter', () => {
                tooltipTimeout = setTimeout(() => {
                    const tooltipText = container.getAttribute('data-tooltip');
                    tooltipElement = document.createElement('div');
                    tooltipElement.className = 'tooltip';
                    tooltipElement.innerText = tooltipText;
                    document.body.appendChild(tooltipElement);

                    const containerRect = container.getBoundingClientRect();
                    const tooltipRect = tooltipElement.getBoundingClientRect();

                    const top = containerRect.top - tooltipRect.height - 8;
                    const left = containerRect.left + (containerRect.width / 2) - (tooltipRect.width / 2);

                    tooltipElement.style.top = `${top}px`;
                    tooltipElement.style.left = `${left}px`;

                    requestAnimationFrame(() => {
                        tooltipElement.classList.add('show');
                    });
                }, 200);
            });

mouseleaveのイベントを書いていきます。

ホバーされなくなったタイミングで、clearTimeoutを呼び出して、セットしたタイムアウトを解除します。その後、showクラスを削除することで非表示にします。
さらに一時的に作成したツールチップのDOMを削除しています。

            container.addEventListener('mouseleave', () => {
                clearTimeout(tooltipTimeout);
                if (tooltipElement) {
                    tooltipElement.classList.remove('show');
                    tooltipElement.addEventListener('transitionend', () => {
                        if (tooltipElement.parentElement) {
                            tooltipElement.parentElement.removeChild(tooltipElement);
                        }
                    }, { once: true });
                }
            });

JavaScriptをまとめると下記になります。

    document.addEventListener('DOMContentLoaded', () => {
        const tooltipContainers = document.querySelectorAll('[data-tooltip]');

        tooltipContainers.forEach(container => {
            let tooltipTimeout;
            let tooltipElement;

            container.addEventListener('mouseenter', () => {
                tooltipTimeout = setTimeout(() => {
                    const tooltipText = container.getAttribute('data-tooltip');
                    tooltipElement = document.createElement('div');
                    tooltipElement.className = 'tooltip';
                    tooltipElement.innerText = tooltipText;
                    document.body.appendChild(tooltipElement);

                    const containerRect = container.getBoundingClientRect();
                    const tooltipRect = tooltipElement.getBoundingClientRect();

                    const top = containerRect.top - tooltipRect.height - 8;
                    const left = containerRect.left + (containerRect.width / 2) - (tooltipRect.width / 2);

                    tooltipElement.style.top = `${top}px`;
                    tooltipElement.style.left = `${left}px`;

                    requestAnimationFrame(() => {
                        tooltipElement.classList.add('show');
                    });
                }, 200);
            });

            container.addEventListener('mouseleave', () => {
                clearTimeout(tooltipTimeout);
                if (tooltipElement) {
                    tooltipElement.classList.remove('show');
                    tooltipElement.addEventListener('transitionend', () => {
                        if (tooltipElement.parentElement) {
                            tooltipElement.parentElement.removeChild(tooltipElement);
                        }
                    }, { once: true });
                }
            });
        });
    });

Summary

setTimeoutと、clearTimeoutの扱いが個人的なキモでした。

よかったらシェアしてね!
目次