Skip to content

Grid Newsroom Hero

Data hero section built on live numbers: a rolling headline figure, a scrubbable 24 h SVG chart and a market ticker band.

  • Heroes
  • Climate & energy
  • React only · 0 deps
  • 1 file
Full screen

Browser support · All evergreen browsers. Chart scrubs with pointer or arrow keys; ticker pauses on hover and focus.

Install. One command, or copy the file.

$npx shadcn@latest add https://freedesignmd.com/r/hero-data-ticker.json
  • Byte-identical to the preview
  • No Tailwind or config needed
  • SSR-safe · keyboard accessible
  • Respects reduced motion & transparency
components/heroes/hero-data-ticker.tsx
1/**
2 * HeroDataTicker: a newsroom hero for an energy-market publication where the
3 * hero is the data. A giant live number rolls digit by digit as it updates,
4 * the headline quotes it, a 24-hour SVG chart can be scrubbed with the
5 * pointer or arrow keys, a generation-mix table shifts with each refresh and
6 * a price ticker band runs along the bottom.
7 *
8 * Props: title (masthead), dateline, kicker, series (24 h of renewable share,
9 * percent), mix (source, GW, renewable), quotes (ticker), refreshMs, byline.
10 * CSS vars: --fd-tkr-paper, --fd-tkr-ink, --fd-tkr-red.
11 *
12 * Browser support: all evergreen browsers.
13 * Motion: a timer nudges the demo data (seeded, deterministic) every
14 * refreshMs, paused in hidden tabs and off-screen; digits roll with
15 * --fd-ease-out; the ticker band scrolls with a CSS animation and pauses on
16 * hover or focus. prefers-reduced-motion: no live updates, no rolling, and
17 * the ticker becomes a static, horizontally scrollable row.
18 * Accessibility: the number is announced via an aria-live="polite" summary
19 * at most once per refresh; the chart is a focusable figure with an
20 * aria-label and arrow-key scrubbing; the ticker duplicate is aria-hidden.
21 */
22import { useEffect, useMemo, useRef, useState } from "react";
23import type { KeyboardEvent as ReactKeyboardEvent, ReactNode } from "react";
24
25export interface TickerQuote {
26 sym: string;
27 price: number;
28 unit: string;
29 change: number;
30}
31
32export interface MixRow {
33 source: string;
34 gw: number;
35 renewable: boolean;
36}
37
38export interface HeroDataTickerProps {
39 title?: string;
40 dateline?: string;
41 kicker?: string;
42 series?: number[];
43 mix?: MixRow[];
44 quotes?: TickerQuote[];
45 refreshMs?: number;
46 byline?: ReactNode;
47 price?: number;
48}
49
50function mulberry32(seed: number) {
51 let a = seed;
52 return () => {
53 a |= 0;
54 a = (a + 0x6d2b79f5) | 0;
55 let t = Math.imul(a ^ (a >>> 15), 1 | a);
56 t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
57 return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
58 };
59}
60
61// 96 quarter hours, 14:45 yesterday to 14:30 today. Wind builds overnight, solar adds a midday bump.
62function makeSeries(): number[] {
63 const r = mulberry32(1412);
64 const out: number[] = [];
65 let wind = 34;
66 for (let i = 0; i < 96; i++) {
67 const hour = (14.75 + i * 0.25) % 24;
68 wind += (r() - 0.46) * 1.6;
69 wind = Math.max(22, Math.min(52, wind));
70 const solar = hour > 7 && hour < 17 ? Math.sin(((hour - 7) / 10) * Math.PI) * 16 : 0;
71 const base = 9 + (hour > 0 && hour < 5 ? 2 : 0);
72 out.push(Math.round((wind + solar + base) * 10) / 10);
73 }
74 out[94] = 71.0;
75 out[95] = 71.4;
76 return out;
77}
78
79const DEFAULT_MIX: MixRow[] = [
80 { source: "Wind onshore", gw: 31.2, renewable: true },
81 { source: "Wind offshore", gw: 6.9, renewable: true },
82 { source: "Biomass", gw: 4.4, renewable: true },
83 { source: "Solar", gw: 4.1, renewable: true },
84 { source: "Gas", gw: 6.8, renewable: false },
85 { source: "Lignite", gw: 5.9, renewable: false },
86 { source: "Hard coal", gw: 2.1, renewable: false },
87];
88
89const DEFAULT_QUOTES: TickerQuote[] = [
90 { sym: "DE-LU base", price: 84.12, unit: "€/MWh", change: -3.2 },
91 { sym: "FR base", price: 61.4, unit: "€/MWh", change: 1.1 },
92 { sym: "NL base", price: 86.03, unit: "€/MWh", change: -2.4 },
93 { sym: "PL base", price: 112.7, unit: "€/MWh", change: 0.6 },
94 { sym: "ES base", price: 42.18, unit: "€/MWh", change: -7.9 },
95 { sym: "NO2", price: 38.9, unit: "€/MWh", change: 4.3 },
96 { sym: "EUA Dec-26", price: 71.34, unit: "€/t", change: -0.8 },
97 { sym: "TTF front", price: 31.2, unit: "€/MWh", change: 0.4 },
98 { sym: "Cal-27 DE", price: 88.5, unit: "€/MWh", change: -0.3 },
99];
100
101const HOURS = ["15:00", "19:00", "23:00", "03:00", "07:00", "11:00", "14:30"];
102
103function quarterLabel(i: number) {
104 const mins = (14 * 60 + 45 + i * 15) % (24 * 60);
105 return `${String(Math.floor(mins / 60)).padStart(2, "0")}:${String(mins % 60).padStart(2, "0")}`;
106}
107
108function Roll({ value, className }: { value: string; className?: string }) {
109 return (
110 <span className={`fd-tkr-roll ${className ?? ""}`} aria-hidden="true">
111 {Array.from(value).map((ch, i) =>
112 /\d/.test(ch) ? (
113 <span key={i} className="fd-tkr-digit">
114 <span className="fd-tkr-strip" style={{ transform: `translateY(${-Number(ch) * 10}%)` }}>
115 {"0123456789".split("").map((d) => (
116 <span key={d}>{d}</span>
117 ))}
118 </span>
119 </span>
120 ) : (
121 <span key={i} className="fd-tkr-sym">{ch}</span>
122 ),
123 )}
124 </span>
125 );
126}
127
128export function HeroDataTicker({
129 title = "The Baseload",
130 dateline = "Monday 2 November 2026",
131 kicker = "Live, German grid",
132 series: seriesProp,
133 mix: mixProp = DEFAULT_MIX,
134 quotes: quotesProp = DEFAULT_QUOTES,
135 refreshMs = 2600,
136 byline = (
137 <>
138 By <b>Marta Kühn</b>, energy desk. Data refreshed every 15 minutes from grid operator feeds.
139 </>
140 ),
141 price: priceProp = 84.12,
142}: HeroDataTickerProps) {
143 const initialSeries = useMemo(() => seriesProp ?? makeSeries(), [seriesProp]);
144 const [series, setSeries] = useState(initialSeries);
145 const [mix, setMix] = useState(mixProp);
146 const [quotes, setQuotes] = useState(quotesProp);
147 const [price, setPrice] = useState(priceProp);
148 const [stamp, setStamp] = useState(0);
149 const [hover, setHover] = useState<number | null>(null);
150 const [reduce, setReduce] = useState(false);
151 const rootRef = useRef<HTMLElement>(null);
152 const chartRef = useRef<HTMLDivElement>(null);
153
154 const now = series[series.length - 1];
155 const prev = series[series.length - 2];
156 const delta = now - prev;
157
158 useEffect(() => {
159 const rm = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
160 setReduce(rm);
161 if (rm) return;
162 const rand = mulberry32(77);
163 let visible = true;
164 const io = new IntersectionObserver(([e]) => {
165 visible = e.isIntersecting;
166 });
167 if (rootRef.current) io.observe(rootRef.current);
168 const id = window.setInterval(() => {
169 if (!visible || document.hidden) return;
170 const step = (rand() - 0.45) * 1.1;
171 setSeries((s) => {
172 const next = Math.round(Math.max(40, Math.min(92, s[s.length - 1] + step)) * 10) / 10;
173 return [...s.slice(1), next];
174 });
175 setMix((m) =>
176 m.map((row) => ({
177 ...row,
178 gw: Math.max(0.4, Math.round((row.gw + (rand() - 0.5) * (row.renewable ? 0.8 : 0.3)) * 10) / 10),
179 })),
180 );
181 setQuotes((q) =>
182 q.map((x) => {
183 const d = (rand() - 0.5) * 0.012 * x.price;
184 const p = Math.round((x.price + d) * 100) / 100;
185 return { ...x, price: p, change: Math.round((x.change + (d / x.price) * 100) * 10) / 10 };
186 }),
187 );
188 setPrice((p) => Math.round(Math.max(20, p - step * 0.9) * 100) / 100);
189 setStamp((t) => t + 1);
190 }, refreshMs);
191 return () => {
192 window.clearInterval(id);
193 io.disconnect();
194 };
195 }, [refreshMs]);
196
197 // chart geometry (viewBox 0 0 600 240, y 0..100%)
198 const W = 600;
199 const H = 240;
200 const pts = series.map((v, i) => [(i / (series.length - 1)) * W, H - (v / 100) * H] as const);
201 const line = pts.map((p, i) => `${i ? "L" : "M"}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join("");
202 const area = `${line}L${W},${H}L0,${H}Z`;
203 const hi = hover ?? series.length - 1;
204 const hv = series[hi];
205 const maxGw = Math.max(...mix.map((m) => m.gw));
206 const renewGw = mix.filter((m) => m.renewable).reduce((a, b) => a + b.gw, 0);
207 const totalGw = mix.reduce((a, b) => a + b.gw, 0);
208 const minute = 32 + Math.floor(stamp / 6);
209 const timeLabel = `14:${String(Math.min(59, minute)).padStart(2, "0")} CET`;
210
211 const scrubAt = (clientX: number) => {
212 const el = chartRef.current;
213 if (!el) return;
214 const r = el.getBoundingClientRect();
215 const f = Math.min(1, Math.max(0, (clientX - r.left) / r.width));
216 setHover(Math.round(f * (series.length - 1)));
217 };
218 const onChartKey = (e: ReactKeyboardEvent) => {
219 if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
220 e.preventDefault();
221 const dir = e.key === "ArrowLeft" ? -1 : 1;
222 setHover((h) => Math.min(series.length - 1, Math.max(0, (h ?? series.length - 1) + dir * (e.shiftKey ? 8 : 1))));
223 } else if (e.key === "Escape") setHover(null);
224 };
225
226 const fmtQ = (q: TickerQuote) => q.price.toFixed(2);
227 const tickerItems = (hidden: boolean) =>
228 quotes.map((q) => (
229 <li key={q.sym + (hidden ? "-d" : "")} className="fd-tkr-q" aria-hidden={hidden || undefined}>
230 <span className="fd-tkr-q-sym">{q.sym}</span>
231 <span className="fd-tkr-q-p">{fmtQ(q)}</span>
232 <span className="fd-tkr-q-u">{q.unit}</span>
233 <span className={`fd-tkr-q-c ${q.change < 0 ? "is-dn" : "is-up"}`}>
234 {q.change < 0 ? "▼" : "▲"} {Math.abs(q.change).toFixed(1)}%
235 </span>
236 </li>
237 ));
238
239 return (
240 <section ref={rootRef} className={`fd-tkr${reduce ? " is-still" : ""}`} aria-label={`${title}, live grid data`}>
241 <style>{css}</style>
242
243 <header className="fd-tkr-mast">
244 <p className="fd-tkr-date">{dateline}<span>Edition 1,412</span></p>
245 <a className="fd-tkr-title" href="#top">{title}</a>
246 <div className="fd-tkr-acts">
247 <a className="fd-tkr-sign" href="#signin">Sign in</a>
248 <a className="fd-tkr-sub" href="#subscribe">Subscribe, €9 a month</a>
249 </div>
250 </header>
251 <nav className="fd-tkr-nav" aria-label="Sections">
252 {["Grid", "Markets", "Policy", "Storage", "Data desk", "The Weekly"].map((s, i) => (
253 <a key={s} href={`#${s.toLowerCase().replace(/\s/g, "-")}`} className={`fd-tkr-sec${i === 0 ? " is-on" : ""}`} aria-current={i === 0 ? "page" : undefined}>
254 {s}
255 </a>
256 ))}
257 </nav>
258
259 <div className="fd-tkr-main">
260 <div className="fd-tkr-story">
261 <p className="fd-tkr-kicker">
262 <span className="fd-tkr-live" aria-hidden="true" />
263 {kicker}
264 <span className="fd-tkr-time">{timeLabel}</span>
265 </p>
266 <p className="fd-tkr-big">
267 <Roll value={now.toFixed(1)} />
268 <span className="fd-tkr-pct" aria-hidden="true">%</span>
269 <span className={`fd-tkr-delta ${delta < 0 ? "is-dn" : "is-up"}`} aria-hidden="true">
270 {delta < 0 ? "▼" : "▲"} {Math.abs(delta).toFixed(1)} pts
271 <small>vs 15 min ago</small>
272 </span>
273 </p>
274 <span className="fd-tkr-sr" aria-live="polite">
275 Renewable share {now.toFixed(1)} percent, {delta < 0 ? "down" : "up"} {Math.abs(delta).toFixed(1)} points.
276 </span>
277 <h1 className="fd-tkr-head">
278 Germany is running on <mark className="fd-tkr-mark">{now.toFixed(1)}%</mark> renewable power. Day-ahead prices slide to €{price.toFixed(2)} as the wind holds.
279 </h1>
280 <dl className="fd-tkr-stats">
281 <div className="fd-tkr-stat"><dt>Wind output</dt><dd><b>{(mix[0].gw + mix[1].gw).toFixed(1)}</b> GW</dd></div>
282 <div className="fd-tkr-stat"><dt>Day-ahead</dt><dd><b>{price.toFixed(2)}</b> €/MWh</dd></div>
283 <div className="fd-tkr-stat"><dt>Carbon intensity</dt><dd><b>{Math.round(560 - now * 4.9)}</b> g/kWh</dd></div>
284 </dl>
285 <p className="fd-tkr-by">{byline}</p>
286 </div>
287
288 <aside className="fd-tkr-panel" aria-label="Grid data">
289 <div className="fd-tkr-panel-h">
290 <p className="fd-tkr-panel-t">Renewable share, last 24 hours</p>
291 <p className="fd-tkr-read">
292 <span>{quarterLabel(hi)}</span>
293 <b>{hv.toFixed(1)}%</b>
294 </p>
295 </div>
296 <div
297 ref={chartRef}
298 className="fd-tkr-chart"
299 role="img"
300 tabIndex={0}
301 aria-label={`Line chart of renewable share over 24 hours, from ${series[0].toFixed(1)}% to ${now.toFixed(1)}%. Use arrow keys to read values.`}
302 onPointerMove={(e) => scrubAt(e.clientX)}
303 onPointerDown={(e) => scrubAt(e.clientX)}
304 onPointerLeave={() => setHover(null)}
305 onKeyDown={onChartKey}
306 onBlur={() => setHover(null)}
307 >
308 <span className="fd-tkr-y fd-tkr-y-75">75</span>
309 <span className="fd-tkr-y fd-tkr-y-50">50</span>
310 <span className="fd-tkr-y fd-tkr-y-25">25</span>
311 <svg className="fd-tkr-svg" viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" aria-hidden="true">
312 {[0.25, 0.5, 0.75].map((g) => (
313 <line key={g} x1="0" x2={W} y1={H * g} y2={H * g} className={g === 0.5 ? "fd-tkr-grid fd-tkr-grid-50" : "fd-tkr-grid"} vectorEffect="non-scaling-stroke" />
314 ))}
315 <path d={area} className="fd-tkr-area" />
316 <path d={line} className="fd-tkr-line" vectorEffect="non-scaling-stroke" />
317 </svg>
318 <span className="fd-tkr-cross" style={{ left: `${(hi / (series.length - 1)) * 100}%`, opacity: hover === null ? 0 : 1 }} aria-hidden="true" />
319 <span className="fd-tkr-dot" style={{ left: `${(hi / (series.length - 1)) * 100}%`, top: `${100 - hv}%` }} aria-hidden="true" />
320 </div>
321 <div className="fd-tkr-x" aria-hidden="true">
322 {HOURS.map((h) => <span key={h}>{h}</span>)}
323 </div>
324
325 <p className="fd-tkr-cap" id="fd-tkr-mixcap">
326 Generation now <span>{renewGw.toFixed(1)} of {totalGw.toFixed(1)} GW clean</span>
327 </p>
328 <table className="fd-tkr-mix" aria-labelledby="fd-tkr-mixcap">
329 <tbody>
330 {mix.map((m) => (
331 <tr key={m.source} className={m.renewable ? "is-re" : "is-fo"}>
332 <th scope="row">{m.source}</th>
333 <td className="fd-tkr-barcell">
334 <span className="fd-tkr-bar" style={{ transform: `scaleX(${(m.gw / maxGw).toFixed(3)})` }} />
335 </td>
336 <td className="fd-tkr-gw">{m.gw.toFixed(1)}</td>
337 </tr>
338 ))}
339 </tbody>
340 </table>
341 </aside>
342 </div>
343
344 <div className="fd-tkr-band" tabIndex={0} aria-label="Market prices">
345 <span className="fd-tkr-band-l">Markets<br />14:30</span>
346 <div className="fd-tkr-track">
347 <ul className="fd-tkr-run">
348 {tickerItems(false)}
349 {tickerItems(true)}
350 </ul>
351 </div>
352 </div>
353 </section>
354 );
355}
356
357const css = `
358.fd-tkr, .fd-tkr *, .fd-tkr *::before, .fd-tkr *::after { box-sizing: border-box; }
359.fd-tkr :where(h1, p, a, ul, li, dl, dt, dd, b, small, span, div, nav, header, aside, mark, table, tbody, tr, th, td, svg) {
360 margin: 0; padding: 0; border: 0; background: none; font: inherit; color: inherit;
361 letter-spacing: inherit; text-decoration: none; list-style: none; text-align: inherit; border-spacing: 0;
362}
363.fd-tkr {
364 --fd-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
365 --fd-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
366 --fd-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
367 --fd-dur-1: 120ms; --fd-dur-2: 240ms; --fd-dur-3: 420ms; --fd-dur-4: 700ms;
368 --fd-tkr-paper: #F2EEE4;
369 --fd-tkr-ink: #141311;
370 --fd-tkr-grey: #6E6A61;
371 --fd-tkr-rule: rgba(20,19,17,0.22);
372 --fd-tkr-red: #C4161C;
373 --fd-tkr-green: #1E6B3A;
374 --fd-tkr-serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif;
375 --fd-tkr-sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif;
376 --fd-tkr-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
377 position: relative; display: flex; flex-direction: column; min-height: 880px; margin: 0; padding: 0; overflow: hidden;
378 background: var(--fd-tkr-paper); color: var(--fd-tkr-ink); font: 400 15px/1.45 var(--fd-tkr-sans);
379}
380
381/* masthead */
382.fd-tkr-mast { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 24px; padding: 20px 40px 14px; }
383.fd-tkr-date { display: flex; flex-direction: column; gap: 3px; font: 400 12px/1.2 var(--fd-tkr-mono); }
384.fd-tkr-date span { color: var(--fd-tkr-grey); }
385.fd-tkr-title { font: 700 clamp(2rem, 3.4vw, 3rem)/1 var(--fd-tkr-serif); letter-spacing: -0.03em; color: var(--fd-tkr-ink); white-space: nowrap; }
386.fd-tkr-acts { justify-self: end; display: flex; align-items: center; gap: 18px; }
387.fd-tkr-sign { font: 500 14px/1 var(--fd-tkr-sans); }
388.fd-tkr-sign:hover { text-decoration: underline; text-underline-offset: 3px; }
389.fd-tkr-sub { font: 600 13px/1 var(--fd-tkr-sans); padding: 10px 14px; background: var(--fd-tkr-red); color: #FFF; border-radius: 2px; transition: background var(--fd-dur-1), transform var(--fd-dur-2) var(--fd-spring); }
390.fd-tkr-sub:hover { background: #A51217; }
391.fd-tkr-sub:active { transform: scale(0.96); transition-duration: var(--fd-dur-1); }
392.fd-tkr-nav { display: flex; justify-content: center; gap: 28px; margin: 0 40px; padding: 10px 0; border-top: 3px double var(--fd-tkr-ink); border-bottom: 1px solid var(--fd-tkr-ink); overflow-x: auto; scrollbar-width: none; }
393.fd-tkr-sec { position: relative; font: 600 12px/1.2 var(--fd-tkr-sans); letter-spacing: 0.08em; text-transform: uppercase; white-space: nowrap; padding: 2px 0; }
394.fd-tkr-sec.is-on { color: var(--fd-tkr-red); }
395.fd-tkr-sec:hover { text-decoration: underline; text-underline-offset: 4px; }
396
397/* main */
398.fd-tkr-main { flex: 1; display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: 0; margin: 0 40px; }
399.fd-tkr-story { padding: 30px 40px 28px 0; border-right: 1px solid var(--fd-tkr-rule); display: flex; flex-direction: column; justify-content: center; }
400.fd-tkr-kicker { display: flex; align-items: center; gap: 10px; font: 700 12px/1 var(--fd-tkr-sans); letter-spacing: 0.1em; text-transform: uppercase; color: var(--fd-tkr-red); }
401.fd-tkr-live { position: relative; display: block; width: 9px; height: 9px; border-radius: 50%; background: var(--fd-tkr-red); }
402.fd-tkr-live::after { content: ""; position: absolute; inset: -4px; border-radius: 50%; border: 1.5px solid var(--fd-tkr-red); animation: fd-tkr-ping 1.8s var(--fd-ease-out) infinite; }
403@keyframes fd-tkr-ping { from { transform: scale(0.5); opacity: 1; } to { transform: scale(1.8); opacity: 0; } }
404.fd-tkr-time { font: 400 12px/1 var(--fd-tkr-mono); letter-spacing: 0; color: var(--fd-tkr-grey); text-transform: none; padding-left: 10px; border-left: 1px solid var(--fd-tkr-rule); }
405
406.fd-tkr-big { display: flex; align-items: flex-start; gap: 6px; margin: 14px 0 4px; font: 400 clamp(6rem, 16vw, 15.5rem)/0.9 var(--fd-tkr-serif); letter-spacing: -0.045em; font-variant-numeric: tabular-nums lining-nums; color: var(--fd-tkr-ink); }
407.fd-tkr-roll { display: inline-flex; }
408.fd-tkr-digit { display: inline-block; height: 0.9em; overflow: hidden; }
409.fd-tkr-strip { display: flex; flex-direction: column; transition: transform var(--fd-dur-4) var(--fd-ease-out); }
410.fd-tkr-strip span { display: block; height: 0.9em; line-height: 0.9; }
411.fd-tkr-sym { display: inline-block; }
412.fd-tkr-pct { font-size: 0.42em; line-height: 1; margin-top: 0.12em; color: var(--fd-tkr-red); }
413.fd-tkr-delta { align-self: flex-end; display: flex; flex-direction: column; gap: 4px; margin: 0 0 0.25em 18px; font: 600 15px/1 var(--fd-tkr-mono); letter-spacing: 0; white-space: nowrap; }
414.fd-tkr-delta small { font: 400 11px/1 var(--fd-tkr-mono); color: var(--fd-tkr-grey); }
415.fd-tkr-delta.is-up { color: var(--fd-tkr-green); }
416.fd-tkr-delta.is-dn { color: var(--fd-tkr-red); }
417.fd-tkr-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
418.fd-tkr-head { margin-top: 18px; max-width: 26ch; font: 400 clamp(1.75rem, 3vw, 2.75rem)/1.06 var(--fd-tkr-serif); letter-spacing: -0.02em; color: var(--fd-tkr-ink); }
419.fd-tkr-mark { background: linear-gradient(transparent 62%, rgba(196,22,28,0.22) 62%); color: inherit; padding: 0 2px; font-variant-numeric: tabular-nums; }
420.fd-tkr-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin-top: 24px; border-top: 1px solid var(--fd-tkr-ink); border-bottom: 1px solid var(--fd-tkr-rule); }
421.fd-tkr-stat { padding: 10px 14px 12px 0; }
422.fd-tkr-stat + .fd-tkr-stat { padding-left: 14px; border-left: 1px solid var(--fd-tkr-rule); }
423.fd-tkr-stat dt { font: 400 11px/1.2 var(--fd-tkr-mono); color: var(--fd-tkr-grey); margin-bottom: 6px; }
424.fd-tkr-stat dd { font: 400 12px/1 var(--fd-tkr-mono); color: var(--fd-tkr-grey); white-space: nowrap; }
425.fd-tkr-stat b { font: 400 26px/1 var(--fd-tkr-serif); letter-spacing: -0.02em; color: var(--fd-tkr-ink); font-variant-numeric: tabular-nums; margin-right: 2px; }
426.fd-tkr-by { margin-top: 14px; max-width: 56ch; font: 400 13px/1.45 var(--fd-tkr-sans); color: var(--fd-tkr-grey); }
427.fd-tkr-by b { font-weight: 600; color: var(--fd-tkr-ink); }
428
429/* panel */
430.fd-tkr-panel { padding: 26px 0 24px 36px; display: flex; flex-direction: column; justify-content: center; }
431.fd-tkr-panel-h { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; margin-bottom: 12px; }
432.fd-tkr-panel-t { font: 700 12px/1.2 var(--fd-tkr-sans); letter-spacing: 0.08em; text-transform: uppercase; }
433.fd-tkr-read { display: flex; gap: 8px; align-items: baseline; font: 400 12px/1 var(--fd-tkr-mono); color: var(--fd-tkr-grey); font-variant-numeric: tabular-nums; }
434.fd-tkr-read b { font: 700 15px/1 var(--fd-tkr-mono); color: var(--fd-tkr-ink); }
435.fd-tkr-chart { position: relative; height: 210px; border-bottom: 1px solid var(--fd-tkr-ink); cursor: crosshair; touch-action: pan-y; outline: none; }
436.fd-tkr-chart:focus-visible { box-shadow: 0 0 0 2px var(--fd-tkr-paper), 0 0 0 4px var(--fd-tkr-ink); }
437.fd-tkr-svg { position: absolute; inset: 0; display: block; width: 100%; height: 100%; overflow: visible; }
438.fd-tkr-grid { stroke: var(--fd-tkr-rule); stroke-width: 1; }
439.fd-tkr-grid-50 { stroke: var(--fd-tkr-ink); stroke-dasharray: 2 4; stroke-opacity: 0.5; }
440.fd-tkr-area { fill: rgba(20,19,17,0.07); }
441.fd-tkr-line { fill: none; stroke: var(--fd-tkr-ink); stroke-width: 1.75; stroke-linejoin: round; }
442.fd-tkr-y { position: absolute; right: 0; font: 400 10px/1 var(--fd-tkr-mono); color: var(--fd-tkr-grey); transform: translateY(-120%); }
443.fd-tkr-y-75 { top: 25%; } .fd-tkr-y-50 { top: 50%; } .fd-tkr-y-25 { top: 75%; }
444.fd-tkr-cross { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--fd-tkr-red); transition: opacity var(--fd-dur-1); pointer-events: none; }
445.fd-tkr-dot { position: absolute; width: 11px; height: 11px; margin: -5.5px 0 0 -5.5px; border-radius: 50%; background: var(--fd-tkr-red); box-shadow: 0 0 0 3px var(--fd-tkr-paper); pointer-events: none; transition: top var(--fd-dur-3) var(--fd-ease-out); }
446.fd-tkr-x { display: flex; justify-content: space-between; padding-top: 6px; font: 400 10px/1 var(--fd-tkr-mono); color: var(--fd-tkr-grey); }
447
448.fd-tkr-mix { width: 100%; border-collapse: collapse; }
449.fd-tkr-cap { margin-top: 22px; display: flex; justify-content: space-between; gap: 12px; padding-bottom: 8px; border-bottom: 1px solid var(--fd-tkr-ink); font: 700 12px/1.2 var(--fd-tkr-sans); letter-spacing: 0.08em; text-transform: uppercase; text-align: left; }
450.fd-tkr-cap span { font: 400 12px/1.2 var(--fd-tkr-mono); letter-spacing: 0; text-transform: none; color: var(--fd-tkr-grey); }
451.fd-tkr-mix tr { border-bottom: 1px solid var(--fd-tkr-rule); }
452.fd-tkr-mix th { width: 34%; padding: 6px 0; font: 400 13px/1.2 var(--fd-tkr-sans); text-align: left; white-space: nowrap; }
453.fd-tkr-barcell { padding: 0 12px; vertical-align: middle; }
454.fd-tkr-bar { display: block; height: 8px; width: 100%; transform-origin: left; background: var(--fd-tkr-ink); transition: transform var(--fd-dur-4) var(--fd-ease-out); }
455.fd-tkr-mix .is-fo .fd-tkr-bar { background: repeating-linear-gradient(135deg, var(--fd-tkr-grey) 0 1.5px, transparent 1.5px 4px); box-shadow: inset 0 0 0 1px var(--fd-tkr-grey); }
456.fd-tkr-mix .is-fo th { color: var(--fd-tkr-grey); }
457.fd-tkr-gw { width: 48px; text-align: right; font: 400 13px/1.2 var(--fd-tkr-mono); font-variant-numeric: tabular-nums; }
458
459/* ticker band */
460.fd-tkr-band { display: flex; align-items: stretch; background: var(--fd-tkr-ink); color: var(--fd-tkr-paper); outline: none; }
461.fd-tkr-band:focus-visible { box-shadow: inset 0 0 0 2px var(--fd-tkr-red); }
462.fd-tkr-band-l { flex: none; display: flex; flex-direction: column; justify-content: center; padding: 0 18px 0 40px; background: var(--fd-tkr-red); color: #FFF; font: 700 11px/1.25 var(--fd-tkr-mono); letter-spacing: 0.06em; text-transform: uppercase; }
463.fd-tkr-track { position: relative; flex: 1; overflow: hidden; -webkit-mask-image: linear-gradient(90deg, transparent, #000 40px, #000 calc(100% - 40px), transparent); mask-image: linear-gradient(90deg, transparent, #000 40px, #000 calc(100% - 40px), transparent); }
464.fd-tkr-run { display: flex; width: max-content; animation: fd-tkr-run 48s linear infinite; }
465.fd-tkr-band:hover .fd-tkr-run, .fd-tkr-band:focus-within .fd-tkr-run { animation-play-state: paused; }
466@keyframes fd-tkr-run { to { transform: translateX(-50%); } }
467.fd-tkr-q { display: flex; align-items: baseline; gap: 8px; padding: 14px 26px; border-right: 1px solid rgba(242,238,228,0.18); white-space: nowrap; font: 400 13px/1 var(--fd-tkr-mono); font-variant-numeric: tabular-nums; }
468.fd-tkr-q-sym { font: 600 12px/1 var(--fd-tkr-sans); letter-spacing: 0.04em; text-transform: uppercase; }
469.fd-tkr-q-p { font-weight: 700; }
470.fd-tkr-q-u { color: rgba(242,238,228,0.55); font-size: 11px; }
471.fd-tkr-q-c.is-up { color: #7FD39A; }
472.fd-tkr-q-c.is-dn { color: #FF8A80; }
473
474.fd-tkr a:focus-visible { outline: 2px solid var(--fd-tkr-ink); outline-offset: 3px; }
475
476@media (max-width: 1080px) {
477 .fd-tkr-main { grid-template-columns: minmax(0, 1fr); }
478 .fd-tkr-story { border-right: 0; border-bottom: 1px solid var(--fd-tkr-rule); padding-right: 0; }
479 .fd-tkr-panel { padding-left: 0; }
480}
481@media (max-width: 760px) {
482 .fd-tkr { min-height: 0; }
483 .fd-tkr-mast { grid-template-columns: 1fr auto; padding: 16px 16px 12px; gap: 12px; }
484 .fd-tkr-date { display: none; }
485 .fd-tkr-title { font-size: 1.75rem; }
486 .fd-tkr-sign { display: none; }
487 .fd-tkr-sub { padding: 9px 12px; }
488 .fd-tkr-nav { justify-content: flex-start; gap: 20px; margin: 0 16px; }
489 .fd-tkr-main { margin: 0 16px; }
490 .fd-tkr-story { padding: 22px 0 22px; }
491 .fd-tkr-big { font-size: 7.25rem; margin-top: 12px; }
492 .fd-tkr-delta { margin-left: 8px; font-size: 13px; }
493 .fd-tkr-head { font-size: 1.625rem; }
494 .fd-tkr-stat { padding-right: 8px; }
495 .fd-tkr-stat + .fd-tkr-stat { padding-left: 10px; }
496 .fd-tkr-stat b { font-size: 20px; }
497 .fd-tkr-stat dd { white-space: normal; }
498 .fd-tkr-panel { padding: 22px 0 24px; }
499 .fd-tkr-chart { height: 170px; }
500 .fd-tkr-x span:nth-child(even) { visibility: hidden; }
501 .fd-tkr-mix tr:nth-child(n + 6) { display: none; }
502 .fd-tkr-band-l { padding: 0 12px 0 16px; }
503 .fd-tkr-q { padding: 13px 18px; }
504}
505@media (prefers-reduced-motion: reduce) {
506 .fd-tkr-strip, .fd-tkr-bar, .fd-tkr-dot, .fd-tkr-sub { transition: none; }
507 .fd-tkr-live::after { animation: none; opacity: 0; }
508 .fd-tkr-run { animation: none; }
509 .fd-tkr-track { overflow-x: auto; scrollbar-width: none; }
510 .fd-tkr-run .fd-tkr-q[aria-hidden="true"] { display: none; }
511}
512`;
513
514export function Demo() {
515 return <HeroDataTicker />;
516}

More Heroes. Same file contract.

Ledger Card Hero, heroes preview: Fintech hero section with a CSS 3D metal card that tilts to the cursor, then flips and docks on scroll as…

Ledger Card Hero

Fintech hero section with a CSS 3D metal card that tilts to the cursor, then flips and docks on scroll as stats count up.

Agent Terminal Hero, heroes preview: AI hero section with a terminal that types a prompt and streams an agent's plan while a dashboard assembles…

Agent Terminal Hero

AI hero section with a terminal that types a prompt and streams an agent's plan while a dashboard assembles beside it.

Maison Editorial Hero, heroes preview: Editorial fashion hero section: a giant serif masthead over three campaign photos, revealed by clip-path wipes

Maison Editorial Hero

Editorial fashion hero section: a giant serif masthead over three campaign photos, revealed by clip-path wipes.

Questions about Grid Newsroom Hero

Run npx shadcn@latest add https://freedesignmd.com/r/hero-data-ticker.json, or open the page and copy the file into your project. It has zero dependencies besides React. What you copy is the exact file that renders the live demo.

Data hero section built on live numbers: a rolling headline figure, a scrubbable 24 h SVG chart and a market ticker band. It is a free, MIT-licensed React heroes component from the freedesignmd library.

Yes. It is free and MIT-licensed, use, modify and ship it in personal and commercial projects without attribution.

All evergreen browsers. Chart scrubs with pointer or arrow keys; ticker pauses on hover and focus.

Yes. It is a plain React component with its own styles, so it runs in Next.js, Vite, Remix and Lovable projects, and it needs no Tailwind setup. It has zero dependencies besides React.