How to Build a StockTicker Marquee with JavaScriptA stock ticker marquee is a compact, attention-grabbing UI element that displays live or frequently updated stock prices, symbols, and small changes. In this article you’ll learn how to design, build, and deploy a responsive, accessible, and efficient StockTicker marquee using vanilla JavaScript, CSS, and a simple server-side or third-party data source. We’ll cover architecture, data fetching and caching, smooth animation, accessibility, testing, and performance optimization.
Overview and goals
A good StockTicker marquee should:
- Show current symbols, last prices, and change percent in a compact format.
- Update frequently without jarring visual jumps.
- Be responsive and lightweight so it can run on dashboards and websites.
- Be accessible for assistive technologies and keyboard users.
- Handle network issues gracefully and avoid excessive API calls.
This guide builds a marquee that:
- Uses a public or mock API for price updates.
- Animates horizontally in a continuous loop.
- Updates data in-place (smoothly) without restarting the animation.
- Falls back to cached data when network fails.
Project structure
Suggested file layout:
- index.html
- styles.css
- ticker.js
- data-provider.js (optional: wraps API calls)
- server.js (optional: proxy or mock server)
Design considerations
-
Data source
- Use a reliable API (IEX Cloud, Alpha Vantage, Finnhub, Yahoo Finance, etc.) or a dedicated WebSocket feed for real-time data.
- For production, prefer WebSockets or server-sent events for lower latency and fewer requests.
- Respect API rate limits; implement client-side caching and a server proxy when needed.
-
Data model Each ticker item should contain:
- symbol (string)
- price (number)
- change (number)
- changePercent (number)
- timestamp
-
UX & accessibility
- Pause animation on hover and focus.
- Provide keyboard controls to pause/resume.
- Expose text alternatives for screen readers (aria-live regions).
- Use color and icons to indicate up/down changes but not rely on color alone.
-
Performance
- Avoid frequent DOM reflows. Use transforms (translateX) for animation.
- Update only the changed fields instead of rebuilding nodes.
- Use requestAnimationFrame for JavaScript-driven animations when necessary.
HTML markup
Keep the HTML semantic and minimal. Example:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title>StockTicker Marquee</title> <link rel="stylesheet" href="styles.css" /> </head> <body> <header> <h1>Stocks</h1> </header> <section class="ticker-wrap" aria-label="Stock Ticker"> <div class="ticker" id="stock-ticker" role="region" aria-live="polite"></div> </section> <script src="data-provider.js"></script> <script src="ticker.js"></script> </body> </html>
CSS: layout and animation
Use CSS for base styling and to enable smooth GPU-accelerated animation via transform.
:root { --bg: #0f1724; --text: #e6eef8; --muted: #9fb0c8; --up: #16a34a; --down: #ef4444; } body { margin: 0; font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; background: var(--bg); color: var(--text); } .ticker-wrap { overflow: hidden; white-space: nowrap; background: linear-gradient(90deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01)); padding: 8px 12px; } .ticker { display: inline-flex; align-items: center; gap: 28px; will-change: transform; } .ticker-item { display: inline-flex; align-items: center; gap: 8px; font-size: 14px; } .symbol { font-weight: 700; color: var(--text); } .price { color: var(--muted); min-width: 64px; text-align: right; } .change { font-weight: 600; } .change.up { color: var(--up); } .change.down { color: var(--down); } /* Animation container that will be moved from right to left */ .ticker-animated { display: inline-flex; will-change: transform; }
JavaScript: core logic
High-level flow:
- Fetch initial list of symbols and prices.
- Render ticker items into an inner scrolling container.
- Start an infinite animation that translates the inner container leftwards.
- Periodically fetch updates and update DOM fields in place.
- Loop the visible content by duplicating items to create a seamless scroll.
Key points:
- Use CSS transforms for smooth movement.
- Duplicate content to allow continuous looping.
- Update only text content and classes for change direction.
- Throttle update frequency and use exponential backoff on errors.
Example ticker.js (core parts):
// ticker.js const TICKER_ID = 'stock-ticker'; const SYMBOLS = ['AAPL','MSFT','GOOGL','AMZN','TSLA','NVDA','META','INTC']; const UPDATE_INTERVAL = 5000; // ms const SPEED = 60; // pixels per second const tickerEl = document.getElementById(TICKER_ID); let items = []; // current item data let startTime, animationFrameId; let offset = 0; async function fetchPrices(symbols) { // Use data-provider.js which wraps fetch to your API or mock try { const res = await window.DataProvider.getPrices(symbols); return res; // expected { symbol, price, change, changePercent, timestamp }[] } catch (err) { console.error('Price fetch error', err); return null; } } function createItemNode(item) { const el = document.createElement('div'); el.className = 'ticker-item'; el.dataset.symbol = item.symbol; el.innerHTML = ` <span class="symbol">${item.symbol}</span> <span class="price">${item.price.toFixed(2)}</span> <span class="change ${item.change >= 0 ? 'up' : 'down'}"> ${item.change >= 0 ? '▲' : '▼'} ${Math.abs(item.change).toFixed(2)} </span> `; return el; } function render(itemsData) { tickerEl.innerHTML = ''; const container = document.createElement('div'); container.className = 'ticker-animated'; itemsData.forEach(it => container.appendChild(createItemNode(it))); // Duplicate for seamless loop itemsData.forEach(it => container.appendChild(createItemNode(it))); tickerEl.appendChild(container); return container; } function startAnimation(container) { const containerWidth = container.scrollWidth / 2; // width of a single set const duration = containerWidth / SPEED * 1000; let last = performance.now(); function step(now) { const delta = now - last; last = now; offset += (SPEED * delta) / 1000; // px moved if (offset >= containerWidth) offset -= containerWidth; container.style.transform = `translateX(${-offset}px)`; animationFrameId = requestAnimationFrame(step); } animationFrameId = requestAnimationFrame(step); } async function updateLoop() { const data = await fetchPrices(SYMBOLS); if (data) { // Merge updates into items and update DOM nodes in-place data.forEach(d => { const node = tickerEl.querySelector(`.ticker-item[data-symbol="${d.symbol}"]`); if (node) { const priceEl = node.querySelector('.price'); const changeEl = node.querySelector('.change'); const prevPrice = parseFloat(priceEl.textContent) || d.price; priceEl.textContent = d.price.toFixed(2); const change = d.price - prevPrice; changeEl.textContent = `${change >= 0 ? '▲' : '▼'} ${Math.abs(change).toFixed(2)}`; changeEl.classList.toggle('up', change >= 0); changeEl.classList.toggle('down', change < 0); } }); } setTimeout(updateLoop, UPDATE_INTERVAL); } async function init() { const data = await fetchPrices(SYMBOLS) || SYMBOLS.map(s => ({ symbol: s, price: 0, change: 0 })); const container = render(data); startAnimation(container); updateLoop(); // Pause on hover/focus tickerEl.addEventListener('mouseenter', () => cancelAnimationFrame(animationFrameId)); tickerEl.addEventListener('mouseleave', () => startAnimation(container)); } init();
Data provider: simple mock and live fetch
For development you can use a mock provider. For production, wrap a real API and implement caching and error handling.
Example data-provider.js (mock):
// data-provider.js window.DataProvider = { async getPrices(symbols) { // mock: random walk return symbols.map(s => { const base = (Math.random() * 150) + 50; const change = (Math.random() - 0.5) * 2; return { symbol: s, price: base + change, change, changePercent: (change / (base || 1)) * 100, timestamp: Date.now() }; }); } };
For a real API, implement a server-side proxy to hide API keys and combine multiple symbols into a single request.
Accessibility details
- Use role=“region” and aria-label on the ticker container so screen readers can find it.
- Use aria-live=“polite” or “off” depending on how disruptive live updates are.
- Provide a visible and keyboard-focusable pause/play control to stop motion for users with vestibular sensitivity.
- Avoid flashing color changes; use subtle transitions.
Example pause control:
<button id="ticker-toggle" aria-pressed="false">Pause</button>
Add JS to toggle animation and update aria-pressed and button text.
Testing and debugging
- Test with screen readers (NVDA, VoiceOver) to ensure announcements are reasonable.
- Throttle network to test reconnection and caching behavior.
- Use Lighthouse to check performance; aim to minimize layout shifts and wasted CPU.
Production tips
- Use WebSockets or SSE for near real-time updates with lower latency and fewer requests.
- Compress payloads and minimize fields returned by API.
- Batch symbol requests server-side and cache results for a short TTL.
- Respect user preferences for reduced motion:
@media (prefers-reduced-motion: reduce)
and pause animation accordingly. - Monitor API usage and implement exponential backoff on errors.
Example enhancements
- Click to expand a symbol with a mini-chart.
- Add grouping (indices, sectors) and filtering.
- Provide currency conversion and localization for numbers.
- Allow theme/sizing customization via CSS variables.
This covers a full approach to building a StockTicker marquee with JavaScript, from markup and styling to animation, data fetching, and accessibility. Implement the data provider with a real API and secure keys on the server for a production-ready ticker.
Leave a Reply