News Ticker
Headlines crawl along a bar at the bottom of the screen, looping without a seam, behind a label block and with an optional clock on the right. Point at it, or press and hold on a phone, to pause it.
How to use it
- Paste the snippet just before
</head>. - Add
data-news-tickerto a bar with one child per headline.
Options
Add any of these attributes to the same element.
data-label | Text of the block on the left. Default “News”; set it empty for no block. |
data-speed | Crawl speed in pixels per second. Default 70. |
data-color | Colour of the label and the dots between headlines. Default #a855f7. |
data-clock | Show the current time at the right-hand end. |
data-pause="false" | Keep crawling when pointed at or touched. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// News Ticker: headlines crawl along a bar at the bottom of the screen,
// looping seamlessly, with a label block at the left and an optional clock
// at the right. Point at it (or press and hold on a phone) to pause.
// Usage: <div data-news-ticker><span>First headline</span><span>Second headline</span></div>
// Each child is one headline.
// Optional: data-label (text of the block on the left, default "News";
// data-label="" for none), data-speed (px per second, default 70),
// data-color (label colour, default #a855f7), data-clock (show the time on
// the right), data-pause="false" (don't pause on hover or touch).
var DEFAULTS = { label: 'News', speed: 70, color: '#a855f7' };
var css =
'.ntk{display:flex;align-items:stretch;overflow:hidden;background:rgba(10,10,20,.88);color:#fff;line-height:1;font-weight:600;--ntk-c:' + DEFAULTS.color + '}' +
'.ntk-label{flex:none;display:flex;align-items:center;padding:0 0.9em;background:var(--ntk-c);font-weight:800;text-transform:uppercase;letter-spacing:0.08em;font-size:0.85em;position:relative;z-index:1}' +
'.ntk-label::after{content:"";position:absolute;top:0;bottom:0;right:-0.6em;width:0.6em;background:inherit;clip-path:polygon(0 0,100% 50%,0 100%)}' +
'.ntk-view{flex:1;min-width:0;overflow:hidden;position:relative;-webkit-mask-image:linear-gradient(90deg,transparent,#000 1.5em,#000 calc(100% - 1.5em),transparent);mask-image:linear-gradient(90deg,transparent,#000 1.5em,#000 calc(100% - 1.5em),transparent)}' +
'.ntk-track{display:flex;width:max-content;will-change:transform}' +
'.ntk-item{display:flex;align-items:center;white-space:nowrap;padding:0.7em 0}' +
'.ntk-item::after{content:"";display:inline-block;width:0.4em;height:0.4em;margin:0 1.1em;border-radius:50%;background:var(--ntk-c)}' +
'.ntk-clock{flex:none;display:flex;align-items:center;padding:0 0.9em;background:rgba(255,255,255,.1);font-variant-numeric:tabular-nums;font-weight:700}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function pad(n) { return (n < 10 ? '0' : '') + n; }
function setup(el) {
var d = el.dataset;
var heads = Array.prototype.slice.call(el.children).map(function (c) { return c.textContent.trim(); })
.filter(Boolean);
if (!heads.length) heads = [el.textContent.trim()];
el.classList.add('ntk');
el.style.setProperty('--ntk-c', d.color || DEFAULTS.color);
el.textContent = '';
var labelText = d.label !== undefined ? d.label : DEFAULTS.label;
var label = null;
if (labelText) {
label = document.createElement('span');
label.className = 'ntk-label';
label.textContent = labelText;
el.appendChild(label);
}
// Screen readers get the headlines as a plain list; the moving copy is hidden.
var list = document.createElement('ul');
list.style.cssText = 'position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);margin:0;padding:0';
heads.forEach(function (h) { var li = document.createElement('li'); li.textContent = h; list.appendChild(li); });
el.appendChild(list);
var view = document.createElement('div');
view.className = 'ntk-view';
view.setAttribute('aria-hidden', 'true');
var track = document.createElement('div');
track.className = 'ntk-track';
view.appendChild(track);
el.appendChild(view);
var clock = null;
if (d.clock !== undefined && d.clock !== 'false') {
clock = document.createElement('span');
clock.className = 'ntk-clock';
clock.setAttribute('aria-hidden', 'true');
el.appendChild(clock);
var tick = function () {
var now = new Date();
clock.textContent = pad(now.getHours()) + ':' + pad(now.getMinutes());
};
tick();
setInterval(tick, 10000);
}
// One "set" is every headline once. Repeat sets until two of them cover
// the view, so shifting by one set width loops without a seam.
var setW = 0;
function fill() {
track.textContent = '';
var probe = document.createDocumentFragment();
heads.forEach(function (h) {
var s = document.createElement('span');
s.className = 'ntk-item';
s.textContent = h;
probe.appendChild(s);
});
track.appendChild(probe);
setW = track.scrollWidth;
var copies = Math.max(2, Math.ceil((view.clientWidth * 2) / Math.max(1, setW)) + 1);
var base = track.innerHTML;
track.innerHTML = new Array(copies + 1).join(base);
}
fill();
// Web fonts change the text width: measure again once they've loaded.
if (document.fonts && document.fonts.ready) document.fonts.ready.then(fill);
if (reduced()) return;
var speed = +d.speed || DEFAULTS.speed;
var x = 0, last = 0, held = false;
var timer = anime.createTimer({
loop: true,
duration: 1000,
autoplay: false,
onUpdate: function () {
var now = performance.now();
var dt = last ? Math.min(50, now - last) : 16;
last = now;
if (!held) {
x -= (speed * dt) / 1000;
if (setW && x <= -setW) x += setW;
}
track.style.transform = 'translateX(' + x.toFixed(2) + 'px)';
}
});
if (d.pause !== 'false') {
el.addEventListener('pointerenter', function (e) { if (e.pointerType === 'mouse') held = true; });
el.addEventListener('pointerleave', function () { held = false; });
el.addEventListener('pointerdown', function () { held = true; });
el.addEventListener('pointerup', function (e) { if (e.pointerType !== 'mouse') held = false; });
el.addEventListener('pointercancel', function () { held = false; });
}
if (window.ResizeObserver) {
var lastW = view.clientWidth;
new ResizeObserver(function () {
if (Math.abs(view.clientWidth - lastW) < 2) return;
lastW = view.clientWidth;
fill();
if (setW) x = x % setW;
}).observe(view);
}
// The label and bar slide up into place the first time it's seen, and
// the crawl pauses while off screen.
var shown = false;
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) {
if (!shown) {
shown = true;
x = view.clientWidth * 0.35;
anime.animate(el, { y: ['100%', '0%'], opacity: [0, 1], duration: 600, ease: 'out(4)' });
if (label) anime.animate(label, { x: ['-100%', '0%'], duration: 700, delay: 200, ease: 'out(4)' });
}
last = 0;
timer.play();
} else timer.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-news-ticker]').forEach(function (el) {
if (el._ntk) return;
el._ntk = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-news-ticker data-label="Latest" data-clock>
<span>Markets close higher after a quiet week</span>
<span>City council approves the new riverside park</span>
<span>Heatwave expected to break by the weekend</span>
</div>