UI Components Medium
Stock Ticker
A scrolling stock and crypto ticker with real-time price simulations and trend indicators.
Open in Lab
MCP
vanilla-js css react tailwind vue svelte
Targets: TS JS HTML React Vue Svelte
Code
:root {
--ticker-bg: #0f172a;
--ticker-text: #f8fafc;
--ticker-up: #22c55e;
--ticker-down: #ef4444;
--ticker-height: 48px;
--ticker-speed: 30s;
}
.ticker-container {
width: 100%;
height: var(--ticker-height);
background: var(--ticker-bg);
overflow: hidden;
position: relative;
display: flex;
align-items: center;
border-radius: 8px;
font-family: "Inter", system-ui, sans-serif;
}
.ticker-wrapper {
display: flex;
white-space: nowrap;
animation: ticker-scroll var(--ticker-speed) linear infinite;
}
.ticker-wrapper:hover {
animation-play-state: paused;
}
.ticker-item {
display: flex;
align-items: center;
padding: 0 2rem;
font-size: 0.875rem;
font-weight: 600;
color: var(--ticker-text);
}
.symbol {
margin-right: 0.5rem;
color: #94a3b8;
}
.price {
font-family: "JetBrains Mono", monospace;
margin-right: 0.5rem;
}
.change {
font-size: 0.75rem;
display: flex;
align-items: center;
gap: 2px;
}
.change.up {
color: var(--ticker-up);
}
.change.down {
color: var(--ticker-down);
}
@keyframes ticker-scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
/* Masking gradients for smooth edges */
.ticker-container::before,
.ticker-container::after {
content: "";
position: absolute;
top: 0;
width: 100px;
height: 100%;
z-index: 2;
pointer-events: none;
}
.ticker-container::before {
left: 0;
background: linear-gradient(to right, var(--ticker-bg), transparent);
}
.ticker-container::after {
right: 0;
background: linear-gradient(to left, var(--ticker-bg), transparent);
}const stocks = [
{ symbol: "AAPL", price: 182.52, change: 1.25 },
{ symbol: "TSLA", price: 202.64, change: -2.45 },
{ symbol: "BTC", price: 62450.0, change: 5.12 },
{ symbol: "NVDA", price: 785.38, change: 3.21 },
{ symbol: "ETH", price: 3450.25, change: 1.85 },
{ symbol: "MSFT", price: 415.5, change: -0.42 },
{ symbol: "AMZN", price: 178.22, change: 0.88 },
{ symbol: "GOOGL", price: 142.15, change: -1.15 },
];
const tickerWrapper = document.getElementById("ticker-wrapper");
function createTickerItem(stock) {
const item = document.createElement("div");
item.className = "ticker-item";
const isUp = stock.change >= 0;
const changeIcon = isUp ? "▴" : "▾";
const changeClass = isUp ? "up" : "down";
item.innerHTML = `
<span class="symbol">${stock.symbol}</span>
<span class="price">$${stock.price.toLocaleString(undefined, { minimumFractionDigits: 2 })}</span>
<span class="change ${changeClass}">${changeIcon} ${Math.abs(stock.change)}%</span>
`;
return item;
}
function initTicker() {
// Add items twice for seamless looping
const items = [...stocks, ...stocks];
items.forEach((stock) => {
tickerWrapper.appendChild(createTickerItem(stock));
});
}
// Simulate price updates
function updatePrices() {
const items = tickerWrapper.querySelectorAll(".ticker-item");
items.forEach((item, index) => {
const stockIndex = index % stocks.length;
const stock = stocks[stockIndex];
// Tiny random movement
const movement = (Math.random() - 0.5) * 0.1;
stock.price += movement;
const priceEl = item.querySelector(".price");
priceEl.textContent = `$${stock.price.toLocaleString(undefined, { minimumFractionDigits: 2 })}`;
});
}
initTicker();
setInterval(updatePrices, 2000);<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Stock Ticker</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="ticker-container">
<div class="ticker-wrapper" id="ticker-wrapper">
<!-- Ticker items will be generated by JS for infinite loop -->
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useState, useEffect } from "react";
const INITIAL_STOCKS = [
{ symbol: "AAPL", name: "Apple", price: 182.52, change: 1.25 },
{ symbol: "TSLA", name: "Tesla", price: 202.64, change: -2.45 },
{ symbol: "BTC", name: "Bitcoin", price: 62450.0, change: 5.12 },
{ symbol: "NVDA", name: "Nvidia", price: 785.38, change: 3.21 },
{ symbol: "ETH", name: "Ethereum", price: 3450.25, change: 1.85 },
{ symbol: "MSFT", name: "Microsoft", price: 415.5, change: -0.42 },
{ symbol: "AMZN", name: "Amazon", price: 178.22, change: 0.88 },
{ symbol: "GOOGL", name: "Alphabet", price: 142.15, change: -1.15 },
];
export default function StockTickerRC() {
const [stocks, setStocks] = useState(INITIAL_STOCKS);
const [flash, setFlash] = useState<Record<string, "up" | "down" | null>>({});
useEffect(() => {
const id = setInterval(() => {
setStocks((prev) =>
prev.map((s) => {
const movement = (Math.random() - 0.48) * 2;
const newPrice = Math.max(0.01, s.price + movement);
return {
...s,
price: newPrice,
change: parseFloat((s.change + (Math.random() - 0.5) * 0.1).toFixed(2)),
};
})
);
}, 2000);
return () => clearInterval(id);
}, []);
return (
<div className="min-h-screen bg-[#0d1117] flex flex-col items-center justify-center p-6 gap-6">
{/* Ticker strip */}
<div className="w-full overflow-hidden bg-[#161b22] border border-[#30363d] rounded-xl py-2">
<div className="flex gap-8 animate-ticker whitespace-nowrap">
{[...stocks, ...stocks].map((s, i) => (
<span key={i} className="inline-flex items-center gap-2 text-sm">
<span className="font-bold text-[#e6edf3]">{s.symbol}</span>
<span className="font-mono text-[#8b949e]">${s.price.toFixed(2)}</span>
<span className={s.change >= 0 ? "text-[#7ee787]" : "text-[#f85149]"}>
{s.change >= 0 ? "▴" : "▾"} {Math.abs(s.change).toFixed(2)}%
</span>
</span>
))}
</div>
</div>
{/* Table */}
<div className="w-full max-w-2xl bg-[#161b22] border border-[#30363d] rounded-xl overflow-hidden">
<div className="grid grid-cols-4 px-4 py-2 border-b border-[#30363d]">
{["Symbol", "Name", "Price", "Change"].map((h) => (
<span key={h} className="text-[11px] text-[#484f58] uppercase tracking-wider">
{h}
</span>
))}
</div>
<div className="divide-y divide-[#21262d]">
{stocks.map((s) => (
<div
key={s.symbol}
className="grid grid-cols-4 px-4 py-3 hover:bg-white/[0.02] transition-colors"
>
<span className="font-bold text-sm text-[#e6edf3]">{s.symbol}</span>
<span className="text-sm text-[#8b949e]">{s.name}</span>
<span className="font-mono text-sm text-[#e6edf3] tabular-nums">
$
{s.price.toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</span>
<span
className={`text-sm font-semibold tabular-nums ${s.change >= 0 ? "text-[#7ee787]" : "text-[#f85149]"}`}
>
{s.change >= 0 ? "▴" : "▾"} {Math.abs(s.change).toFixed(2)}%
</span>
</div>
))}
</div>
</div>
<style>{`
@keyframes ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } }
.animate-ticker { animation: ticker 20s linear infinite; }
`}</style>
</div>
);
}<script setup>
import { ref, computed, onMounted, onUnmounted } from "vue";
const INITIAL_STOCKS = [
{ symbol: "AAPL", name: "Apple", price: 182.52, change: 1.25 },
{ symbol: "TSLA", name: "Tesla", price: 202.64, change: -2.45 },
{ symbol: "BTC", name: "Bitcoin", price: 62450.0, change: 5.12 },
{ symbol: "NVDA", name: "Nvidia", price: 785.38, change: 3.21 },
{ symbol: "ETH", name: "Ethereum", price: 3450.25, change: 1.85 },
{ symbol: "MSFT", name: "Microsoft", price: 415.5, change: -0.42 },
{ symbol: "AMZN", name: "Amazon", price: 178.22, change: 0.88 },
{ symbol: "GOOGL", name: "Alphabet", price: 142.15, change: -1.15 },
];
const stocks = ref([...INITIAL_STOCKS]);
const doubled = computed(() => [...stocks.value, ...stocks.value]);
const headers = ["Symbol", "Name", "Price", "Change"];
function formatPrice(price) {
return price.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
let interval;
onMounted(() => {
const style = document.createElement("style");
style.textContent = `
@keyframes ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } }
.st-row:hover { background: rgba(255,255,255,0.02); }
.st-row:last-child { border-bottom: none !important; }
`;
document.head.appendChild(style);
interval = setInterval(() => {
stocks.value = stocks.value.map((s) => {
const movement = (Math.random() - 0.48) * 2;
const newPrice = Math.max(0.01, s.price + movement);
return {
...s,
price: newPrice,
change: parseFloat((s.change + (Math.random() - 0.5) * 0.1).toFixed(2)),
};
});
}, 2000);
});
onUnmounted(() => {
clearInterval(interval);
});
</script>
<template>
<div :style="{ minHeight:'100vh', background:'#0d1117', display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', padding:'1.5rem', gap:'1.5rem' }">
<!-- Ticker strip -->
<div :style="{ width:'100%', overflow:'hidden', background:'#161b22', border:'1px solid #30363d', borderRadius:'0.75rem', padding:'0.5rem 0' }">
<div :style="{ display:'flex', gap:'2rem', whiteSpace:'nowrap', animation:'ticker 20s linear infinite' }">
<span v-for="(s, i) in doubled" :key="i" :style="{ display:'inline-flex', alignItems:'center', gap:'0.5rem', fontSize:'0.875rem' }">
<span :style="{ fontWeight:700, color:'#e6edf3' }">{{ s.symbol }}</span>
<span :style="{ fontFamily:'monospace', color:'#8b949e' }">${{ s.price.toFixed(2) }}</span>
<span :style="{ color: s.change >= 0 ? '#7ee787' : '#f85149' }">
{{ s.change >= 0 ? "▴" : "▾" }} {{ Math.abs(s.change).toFixed(2) }}%
</span>
</span>
</div>
</div>
<!-- Table -->
<div :style="{ width:'100%', maxWidth:'672px', background:'#161b22', border:'1px solid #30363d', borderRadius:'0.75rem', overflow:'hidden' }">
<div :style="{ display:'grid', gridTemplateColumns:'repeat(4,1fr)', padding:'0.5rem 1rem', borderBottom:'1px solid #30363d' }">
<span v-for="h in headers" :key="h" :style="{ fontSize:'11px', color:'#484f58', textTransform:'uppercase', letterSpacing:'0.05em' }">{{ h }}</span>
</div>
<div>
<div v-for="s in stocks" :key="s.symbol" class="st-row" :style="{ display:'grid', gridTemplateColumns:'repeat(4,1fr)', padding:'0.75rem 1rem', borderBottom:'1px solid #21262d', transition:'background 0.15s' }">
<span :style="{ fontWeight:700, fontSize:'0.875rem', color:'#e6edf3' }">{{ s.symbol }}</span>
<span :style="{ fontSize:'0.875rem', color:'#8b949e' }">{{ s.name }}</span>
<span :style="{ fontFamily:'monospace', fontSize:'0.875rem', color:'#e6edf3', fontVariantNumeric:'tabular-nums' }">${{ formatPrice(s.price) }}</span>
<span :style="{ fontSize:'0.875rem', fontWeight:600, fontVariantNumeric:'tabular-nums', color: s.change >= 0 ? '#7ee787' : '#f85149' }">
{{ s.change >= 0 ? "▴" : "▾" }} {{ Math.abs(s.change).toFixed(2) }}%
</span>
</div>
</div>
</div>
</div>
</template><script>
import { onMount, onDestroy } from "svelte";
const INITIAL_STOCKS = [
{ symbol: "AAPL", name: "Apple", price: 182.52, change: 1.25 },
{ symbol: "TSLA", name: "Tesla", price: 202.64, change: -2.45 },
{ symbol: "BTC", name: "Bitcoin", price: 62450.0, change: 5.12 },
{ symbol: "NVDA", name: "Nvidia", price: 785.38, change: 3.21 },
{ symbol: "ETH", name: "Ethereum", price: 3450.25, change: 1.85 },
{ symbol: "MSFT", name: "Microsoft", price: 415.5, change: -0.42 },
{ symbol: "AMZN", name: "Amazon", price: 178.22, change: 0.88 },
{ symbol: "GOOGL", name: "Alphabet", price: 142.15, change: -1.15 },
];
let stocks = [...INITIAL_STOCKS];
$: doubled = [...stocks, ...stocks];
const headers = ["Symbol", "Name", "Price", "Change"];
function formatPrice(price) {
return price.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
let interval;
onMount(() => {
interval = setInterval(() => {
stocks = stocks.map((s) => {
const movement = (Math.random() - 0.48) * 2;
const newPrice = Math.max(0.01, s.price + movement);
return {
...s,
price: newPrice,
change: parseFloat((s.change + (Math.random() - 0.5) * 0.1).toFixed(2)),
};
});
}, 2000);
});
onDestroy(() => {
clearInterval(interval);
});
</script>
<div class="page">
<!-- Ticker strip -->
<div class="ticker-strip">
<div class="ticker-track">
{#each doubled as s, i (i)}
<span class="ticker-item">
<span class="ticker-symbol">{s.symbol}</span>
<span class="ticker-price">${s.price.toFixed(2)}</span>
<span class={s.change >= 0 ? 'ticker-up' : 'ticker-down'}>
{s.change >= 0 ? '▴' : '▾'} {Math.abs(s.change).toFixed(2)}%
</span>
</span>
{/each}
</div>
</div>
<!-- Table -->
<div class="table">
<div class="table-header">
{#each headers as h}
<span class="table-heading">{h}</span>
{/each}
</div>
<div class="table-body">
{#each stocks as s (s.symbol)}
<div class="table-row">
<span class="row-symbol">{s.symbol}</span>
<span class="row-name">{s.name}</span>
<span class="row-price">${formatPrice(s.price)}</span>
<span class="row-change" class:up={s.change >= 0} class:down={s.change < 0}>
{s.change >= 0 ? '▴' : '▾'} {Math.abs(s.change).toFixed(2)}%
</span>
</div>
{/each}
</div>
</div>
</div>
<style>
.page {
min-height: 100vh;
background: #0d1117;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1.5rem;
gap: 1.5rem;
font-family: system-ui, -apple-system, sans-serif;
box-sizing: border-box;
}
.ticker-strip {
width: 100%;
overflow: hidden;
background: #161b22;
border: 1px solid #30363d;
border-radius: 0.75rem;
padding: 0.5rem 0;
}
.ticker-track {
display: flex;
gap: 2rem;
white-space: nowrap;
animation: ticker 20s linear infinite;
}
.ticker-item {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
}
.ticker-symbol { font-weight: 700; color: #e6edf3; }
.ticker-price { font-family: monospace; color: #8b949e; }
.ticker-up { color: #7ee787; }
.ticker-down { color: #f85149; }
.table {
width: 100%;
max-width: 672px;
background: #161b22;
border: 1px solid #30363d;
border-radius: 0.75rem;
overflow: hidden;
}
.table-header {
display: grid;
grid-template-columns: repeat(4, 1fr);
padding: 0.5rem 1rem;
border-bottom: 1px solid #30363d;
}
.table-heading {
font-size: 11px;
color: #484f58;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.table-body {
display: flex;
flex-direction: column;
}
.table-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
padding: 0.75rem 1rem;
border-bottom: 1px solid #21262d;
transition: background 0.15s;
}
.table-row:hover { background: rgba(255, 255, 255, 0.02); }
.table-row:last-child { border-bottom: none; }
.row-symbol { font-weight: 700; font-size: 0.875rem; color: #e6edf3; }
.row-name { font-size: 0.875rem; color: #8b949e; }
.row-price { font-family: monospace; font-size: 0.875rem; color: #e6edf3; font-variant-numeric: tabular-nums; }
.row-change {
font-size: 0.875rem;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.row-change.up { color: #7ee787; }
.row-change.down { color: #f85149; }
@keyframes ticker {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
</style>Stock Ticker
A dynamic, scrolling ticker component for financial dashboards and landing pages. It features smooth marquee animations, price change indicators (bullish/bearish), and simulated real-time updates.
Features
- Infinite horizontal scrolling (marquee effect)
- Dynamic price updating simulations
- Visual trend indicators (green/red)
- Pause on hover for easier reading
- Customizable ticker items (Stocks, Crypto, Forex)