[JavaScript] Creating a callout DOM linked to the movement of the target element (responsive).

Display callouts with arrows below certain elements. The requirements are summarised as follows.

  • A callout appears from below certain elements.
  • There is an arrow above the callout.
  • The arrow on the callout always appears from the centre of the specific element.
  • If a specific element moves, e.g. by Window width, the callout will follow. (Responsive support)

The completed image looks like this.

目次

Implementing dynamic callouts

Implement a callout that follows a specific element.

Creating elements in HTML

First, consider the HTML.
Identify the specific element (the element you want the arrow to follow) with id=‘target’.
The callout element has been given id=‘balloon’. The HTML for the balloon is simple: the div with class=‘balloon-content’ is the text element and the div with class=‘balloon-arrow’ is the arrow element.

<body>
    <div class="box">ターゲットのテキスト「<span id="target">X</span>」</div>
    <div id="balloon" class="balloon">
        <div class="balloon-content">吹き出しの表示エリア。吹き出しの表示エリア。</div>
        <div class="balloon-arrow"></div>
    </div>
</body>

Decorating with CSS

Decorating balloons with CSS.
Nothing special to mention, but the arrows in the .balloon-arrow class are created with the border property.

    body {
        height: 100vh;
    }

    .box {
        width: fit-content;
        padding: 8px 16px;
        background-color: #3b3b3b;
        color: #fff;
        position: relative;
        text-wrap: wrap;
        display: inline-block;
    }

    .balloon {
        position: absolute;
        background-color: #007bff;
        color: #fff;
        padding: 10px;
        border-radius: 5px;
        display: none;
        max-width: 320px;
        z-index: 1000;
    }

    .balloon-arrow {
        position: absolute;
        width: 0;
        height: 0;
        border-left: 8px solid transparent;
        border-right: 8px solid transparent;
        border-top: 8px solid #007bff;
        top: -8px;
        transform: translateX(-50%) rotate(180deg);
    }

    .balloon-content {
        font-size: 14px;
    }

Dynamically change the callout position with JavaScript.

Allow arrows and callouts to change position dynamically, even when responsive with JavaScript.

The getOverlayPosition function returns the callout position.

getOverlayPosition function.
Here, the position of the callout with respect to the target element is calculated, and the top and left positions of the callout in Object, and the horizontal centre of the target element (used by the arrow in the callout). The callout returns the

The target element and the callout element are passed in HTMLElement.
Then, in this function, the screen position is obtained by getBoundingClientRect.
The bottom position of the target element becomes the top position of the callout.

The left position of the callout is calculated in detail.
targetRect.left + (targetRect.width / 2) to get the absolute coordinates of the centre of the target element,
targetRect.left + (targetRect.width / 2) - (overlayRect.width / 2) to subtract half the width of the overlay element from the absolute coordinates of the centre of the target element. This aligns the left edge of the overlay element with the centre of the target element.

Furthermore, the left position of the callout is adjusted so that it does not exceed the clientWidth when going to the right or left edge.

    /**
     * Calculate the position of the overlay relative to the target element.
     * Returns a value that falls within the document.body
     *
     * @param {HTMLElement} target - Target elements of the starting point
     * @param {HTMLElement} overlay - Overlay elements to be placed
     * @returns {Object} 
     * @returns {number} top - オーバーレイの一番上の位置
     * @returns {number} left - オーバーレイの左位置。
     * @returns {number} targetCenter - ターゲット要素の水平方向の中心。
     */
    function getOverlayPosition(target, overlay) {
        const targetRect = target.getBoundingClientRect();
        const overlayRect = overlay.getBoundingClientRect();
        const bodyWidth = document.body.clientWidth;

        let top = targetRect.bottom;
        let left = targetRect.left + (targetRect.width / 2) - (overlayRect.width / 2);

        // オーバーレイが右端を超えないように調整
        if (left + overlayRect.width > bodyWidth) {
            left = bodyWidth - overlayRect.width;
        }

        // オーバーレイが左端を超えないように調整
        if (left < 0) {
            left = 0;
        }

        return { top, left, targetCenter: targetRect.left + (targetRect.width / 2) };
    }

Position the callout dynamically with the positionOverlay function.

In the positionOverlay function, the target element Id and callout element Id strings are passed and getOverlayPosition is called internally to dynamically display the callout.

    /**
     * Position the overlay relative to the target element.
     *
     * @param {string} targetId - ターゲットId.
     * @param {string} overlayId - オーバーレイId.
     */
    function positionOverlay(targetId, overlayId) {
        const target = document.getElementById(targetId);
        const overlay = document.getElementById(overlayId);
        const arrow = overlay.querySelector('.balloon-arrow');

        if (target && overlay) {
            overlay.style.display = 'block';

            const { top, left, targetCenter } = getOverlayPosition(target, overlay);
            overlay.style.position = 'absolute';
            overlay.style.top = `${top + 16}px`; // add offset
            overlay.style.left = `${left}px`;

            // 矢印の位置を設定
            if (arrow) {
                arrow.style.left = `${targetCenter - left}px`;
            }
        }
    }

Finally, it is called to work on initial rendering and on responsive.

    window.addEventListener('DOMContentLoaded', function () {
        positionOverlay('target', 'balloon');
        window.addEventListener('resize', () => positionOverlay('target', 'balloon'));
    });

This will create a responsive callout.

Summary

It is easier to organise DOM position calculations when they are done on paper. There were times when I got lost, wondering where the left position was from, or where the calculated value was. It would be better to put them in variables or comment them so they don’t get lost.

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