Home About Me

Building a Seamless Vertical Notice Ticker with Vanilla JavaScript

Vertical notice tickers look simple until you try to build one without a library. A quick demo is enough to see the intended motion: a fixed-height notice box shows one line at a time; the current item slides upward, disappears, and is appended to the end of the list so the cycle can continue.

Most ready-made seamless ticker styles available online depend on jQuery, so the implementation below rewrites that approach in native JavaScript. The core idea is to animate marginTop from 0 to -35px over 500ms, reset marginTop to 0 when the animation finishes, and move the first list item to the end of the list. The CSS container is 35px high with overflow: hidden, which makes the item appear to slide out of view while the next item rises into place.

The jQuery version being replaced

The original animation is compact: animate() moves the list upward, and the callback resets the margin and recycles the first item.

/*
* Parameters:
* obj: the animated node, a ul in this example
* top: animation height, -35px here; upward scrolling uses a negative value
* time: animation duration in milliseconds, 500 here, meaning marginTop goes from 0 to -35px in 500 ms
* function: callback; after each animation marginTop resets to zero and the first item is appended to the end of the list
*
*/
function noticeUp(obj,top,time) {
 $(obj).animate({
 marginTop: top
 }, time, function () {
 $(this).css({marginTop:"0"}).find(":first").appendTo(this);
 })
}

jQuery is undeniably convenient here. The native version below takes several times more code, mainly because the animation has to be driven by hand.

A native JavaScript version

The rewritten function uses requestAnimationFrame() and an easing helper. On each frame it calculates the current marginTop; when the duration is complete, it sets marginTop back to 0px and appends the first element to the end of the list.

function noticeUp(obj, top, time) {
    var element = document.querySelector(obj);
    var start = null;
    var targetMarginTop = parseInt(top, 10);
    var initialMarginTop = parseInt(window.getComputedStyle(element).marginTop, 10);
    var changeInValue = targetMarginTop - initialMarginTop;
    var currentTime = 0;
    var increment = 20;
    var animateMargin = function (timestamp) {
        if (!start) start = timestamp;
        currentTime += increment;
        var val = Math.easeInOutQuad(currentTime, initialMarginTop, changeInValue, time);
        element.style.marginTop = val + 'px';
        if (currentTime < time) {
            requestAnimationFrame(animateMargin);
        } else {
            element.style.marginTop = '0px';
            element.appendChild(element.firstElementChild);
        }
    };
    // t = current time, b = start value, c = change in value, d = duration
    Math.easeInOutQuad = function (t, b, c, d) {
        t /= d / 2;
        if (t < 1) return c / 2 * t * t + b;
        t--;
        return -c / 2 * (t * (t - 2) - 1) + b;
    };
    requestAnimationFrame(animateMargin);
}

The easing function is the same easeInOutQuad calculated from t, b, c, and d. The callback is replaced by a check on currentTime < time; when that condition no longer holds, the animation stops and the first child is recycled.

Full page example

The complete page combines the fixed-height container, a single-line list, the native animation function, and a setInterval that calls it every two seconds.

<!DOCTYPE html>
<html>

<head lang="en">
    <meta charset="UTF-8">
    <title>文字从下到上无缝轮播</title>
    <style>
        div,
        ul,
        li {
            margin: 0;
            padding: 0
        }

        /* Initialize default styles first */
        .notice {
            width: 300px;
            /* Single-line display with overflow hidden */
            height: 35px;
            /* Fixed height for the notice area */
            padding: 0 30px;
            background-color: #b3effe;
            overflow: hidden;
        }

        .notice ul li {
            list-style: none;
            line-height: 35px;
            /* Below keeps it to a single line with overflow hidden */
            display: block;
            white-space: nowrap;
            text-overflow: ellipsis;
            overflow: hidden;
        }
    </style>
</head>

<body>
    <div class="notice">
        <ul>
            <li>第1条公告第1条公告第1条公告第1条公告第1条公告第1条公告</li>
            <li>第2条公告第2条公告第2条公告第2条公告第2条公告第2条公告</li>
            <li>第3条公告第3条公告第3条公告第3条公告第3条公告第3条公告</li>
            <li>第4条公告第4条公告第4条公告第4条公告第4条公告第4条公告</li>
        </ul>
    </div>
    <script>
        function noticeUp(obj, top, time) {
            var element = document.querySelector(obj);
            var start = null;
            var targetMarginTop = parseInt(top, 10);
            var initialMarginTop = parseInt(window.getComputedStyle(element).marginTop, 10);
            var changeInValue = targetMarginTop - initialMarginTop;
            var currentTime = 0;
            var increment = 20;

            var animateMargin = function (timestamp) {
                if (!start) start = timestamp;
                currentTime += increment;
                var val = Math.easeInOutQuad(currentTime, initialMarginTop, changeInValue, time);
                element.style.marginTop = val + 'px';
                if (currentTime < time) {
                    requestAnimationFrame(animateMargin);
                } else {
                    element.style.marginTop = '0px';
                    element.appendChild(element.firstElementChild);
                }
            };

            // t = current time, b = start value, c = change in value, d = duration
            Math.easeInOutQuad = function (t, b, c, d) {
                t /= d / 2;
                if (t < 1) return c / 2 * t * t + b;
                t--;
                return -c / 2 * (t * (t - 2) - 1) + b;
            };

            requestAnimationFrame(animateMargin);
        }


        setInterval("noticeUp('.notice ul','-35px',500)", 2000);

    </script>
</body>

</html>

The list item text is left as placeholder Chinese strings; replace them with the announcements you need. The timing is also easy to adjust: -35px matches the 35px container height, 500 is the animation duration in milliseconds, and the interval 2000 controls how long each item stays visible before the next move.