number-tickerNumber ticker
数字滚动Numeric glyphs translate or interpolate until the display reaches an updated value.
数字滚动或递增到目标值。
number-tickerShow changing values with tabular figures and stable vertical digit movement.
Numeric glyphs translate or interpolate until the display reaches an updated value.
Implement Number ticker: Show changing values with tabular figures and stable vertical digit movement. Use Duration 240ms, Distance 24px, Easing snap. Trigger it once when state changes and keep it replayable. Use semantic HTML, scope CSS to .motion-demo--number-ticker, and mount the provided vanilla JavaScript interaction runtime. Provide equivalent low-motion behavior for prefers-reduced-motion and .force-reduced-motion.
<div class="motion-demo motion-demo--number-ticker" data-motion="number-ticker" aria-label="Number ticker demo">
<div class="motion-replay-scene">
<div class="motion-number" aria-label="128"><span class="motion-number-track">128</span></div>
<div class="motion-action-row"><button class="motion-replay-button" type="button" data-motion-replay>Replay motion</button></div>
</div>
</div>.motion-demo.motion-demo--number-ticker {
box-sizing: border-box;
position: relative;
display: grid;
place-items: center;
width: min(100%, 34rem);
min-height: 15rem;
padding: 2rem;
overflow: hidden;
color: #18181b;
background: #fafafa;
border: 1px solid #e4e4e7;
border-radius: 16px;
isolation: isolate;
}
.motion-demo.motion-demo--number-ticker *, .motion-demo.motion-demo--number-ticker *::before, .motion-demo.motion-demo--number-ticker *::after {
box-sizing: border-box;
}
.motion-demo.motion-demo--number-ticker .motion-surface {
position: relative;
display: grid;
gap: 0.65rem;
width: min(100%, 18rem);
padding: 1.25rem;
background: #ffffff;
border: 1px solid #e4e4e7;
border-radius: 16px;
box-shadow: none;
}
.motion-demo.motion-demo--number-ticker .motion-surface strong { font-weight: 500; }
.motion-demo.motion-demo--number-ticker .motion-line {
display: block;
width: 100%;
height: 0.5rem;
background: #e4e4e7;
border-radius: 999px;
}
.motion-demo.motion-demo--number-ticker .motion-line:last-child { width: 62%; }
.motion-demo.motion-demo--number-ticker .motion-button {
position: relative;
min-height: 2.75rem;
padding: 0.75rem 1.1rem;
color: white;
font: inherit;
font-weight: 700;
background: #18181b;
border: 0;
border-radius: 999px;
cursor: pointer;
}
.motion-demo.motion-demo--number-ticker .motion-button:focus-visible { outline: 3px solid rgb(37 99 235 / 32%); outline-offset: 3px; }
.motion-demo.motion-demo--number-ticker .motion-action-row {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-top: 0.85rem;
}
.motion-demo.motion-demo--number-ticker .motion-replay-scene {
width: min(100%, 22rem);
display: grid;
place-items: center;
}
.motion-demo.motion-demo--number-ticker .motion-pausable-scene {
width: min(100%, 22rem);
display: grid;
place-items: center;
}
.motion-demo.motion-demo--number-ticker button.motion-surface {
color: inherit;
font: inherit;
text-align: start;
cursor: pointer;
}
.motion-demo.motion-demo--number-ticker .motion-replay-button,
.motion-demo.motion-demo--number-ticker .motion-undo-button,
.motion-demo.motion-demo--number-ticker .motion-dismiss-button,
.motion-demo.motion-demo--number-ticker .motion-pause-button {
min-height: 2.75rem;
padding: 0.65rem 0.9rem;
color: #18181b;
font: 650 0.78rem/1 ui-sans-serif, system-ui, sans-serif;
background: #ffffff;
border: 1px solid #d4d4d8;
border-radius: 8px;
cursor: pointer;
}
.motion-demo.motion-demo--number-ticker .motion-replay-button:focus-visible,
.motion-demo.motion-demo--number-ticker .motion-undo-button:focus-visible,
.motion-demo.motion-demo--number-ticker .motion-dismiss-button:focus-visible,
.motion-demo.motion-demo--number-ticker .motion-pause-button:focus-visible,
.motion-demo.motion-demo--number-ticker [data-reorder-item]:focus-visible,
.motion-demo.motion-demo--number-ticker [data-swipe-target]:focus-visible {
outline: 3px solid rgb(37 99 235 / 32%);
outline-offset: 3px;
}
.motion-demo.motion-demo--number-ticker .motion-status {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.motion-demo.motion-demo--number-ticker .motion-ripple-ink {
position: absolute;
width: 12px;
height: 12px;
pointer-events: none;
background: rgb(255 255 255 / 72%);
border-radius: 50%;
transform: translate(-50%, -50%) scale(0.95);
opacity: 0;
}
.motion-demo.motion-demo--number-ticker .motion-state-stack { position: relative; width: min(100%, 18rem); height: 9rem; }
.motion-demo.motion-demo--number-ticker .motion-state { position: absolute; inset: 0; display: grid; place-items: center; padding: 1rem; background: #ffffff; border: 1px solid #e4e4e7; border-radius: 16px; }
.motion-demo.motion-demo--number-ticker .motion-state--to { background: #18181b; color: white; }
.motion-demo.motion-demo--number-ticker .motion-scroll-track > span { color: #71717a; font: 600 0.72rem/1.2 ui-monospace, monospace; letter-spacing: 0.08em; text-transform: uppercase; }
.motion-demo.motion-demo--number-ticker .motion-orbit-item { display: grid; place-items: center; width: 2.5rem; height: 2.5rem; color: white; background: #2563eb; border-radius: 50%; }
.motion-demo.motion-demo--number-ticker .motion-text-stack { position: relative; width: min(100%, 18rem); min-height: 4rem; }
.motion-demo.motion-demo--number-ticker svg { width: min(100%, 20rem); height: auto; }
.motion-demo.motion-demo--number-ticker .motion-number { height: 1.2em; overflow: hidden; font: 700 3rem/1.2 ui-monospace, monospace; font-variant-numeric: tabular-nums; }
.motion-demo.motion-demo--number-ticker .motion-number-track { display: grid; animation: motion-number-ticker 240ms cubic-bezier(0.16, 1, 0.3, 1) both; }
@keyframes motion-number-ticker { from { transform: translateY(24px); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes motion-number-ticker-reduced-fade {
from { opacity: 0.84; }
to { opacity: 1; }
}
.force-reduced-motion .motion-demo.motion-demo--number-ticker {
animation: motion-number-ticker-reduced-fade 160ms cubic-bezier(0.23, 1, 0.32, 1) both !important;
}
.force-reduced-motion .motion-demo.motion-demo--number-ticker *,
.force-reduced-motion .motion-demo.motion-demo--number-ticker *::before,
.force-reduced-motion .motion-demo.motion-demo--number-ticker *::after {
animation: none !important;
animation-delay: 0ms !important;
animation-iteration-count: 1 !important;
transition-property: opacity, color, background-color, border-color !important;
transition-duration: 160ms !important;
transition-delay: 0ms !important;
scroll-behavior: auto !important;
}
.force-reduced-motion .motion-demo.motion-demo--number-ticker .motion-button,
.force-reduced-motion .motion-demo.motion-demo--number-ticker [data-reorder-item],
.force-reduced-motion .motion-demo.motion-demo--number-ticker [data-swipe-target],
.force-reduced-motion .motion-demo.motion-demo--number-ticker [data-spring-target] {
transform: none !important;
}
.force-reduced-motion .motion-demo.motion-demo--number-ticker .motion-path { stroke-dashoffset: 0 !important; }
.force-reduced-motion .motion-demo.motion-demo--number-ticker .motion-word--from { opacity: 0 !important; visibility: hidden; }
.force-reduced-motion .motion-demo.motion-demo--number-ticker .motion-word:not(.motion-word--from) { opacity: 1 !important; filter: none !important; transform: none !important; }
@media (prefers-reduced-motion: reduce) {
.motion-demo.motion-demo--number-ticker {
animation: motion-number-ticker-reduced-fade 160ms cubic-bezier(0.23, 1, 0.32, 1) both !important;
}
.motion-demo.motion-demo--number-ticker *, .motion-demo.motion-demo--number-ticker *::before, .motion-demo.motion-demo--number-ticker *::after {
animation: none !important;
animation-delay: 0ms !important;
animation-iteration-count: 1 !important;
transition-property: opacity, color, background-color, border-color !important;
transition-duration: 160ms !important;
transition-delay: 0ms !important;
scroll-behavior: auto !important;
}
.motion-demo.motion-demo--number-ticker .motion-button,
.motion-demo.motion-demo--number-ticker [data-reorder-item],
.motion-demo.motion-demo--number-ticker [data-swipe-target],
.motion-demo.motion-demo--number-ticker [data-spring-target] {
transform: none !important;
}
.motion-demo.motion-demo--number-ticker .motion-path { stroke-dashoffset: 0 !important; }
.motion-demo.motion-demo--number-ticker .motion-word--from { opacity: 0 !important; visibility: hidden; }
.motion-demo.motion-demo--number-ticker .motion-word:not(.motion-word--from) { opacity: 1 !important; filter: none !important; transform: none !important; }
}(() => {
const config = {
"id": "number-ticker",
"duration": 240,
"distance": 24,
"resistance": 65,
"position": 50,
"stiffness": 220,
"damping": 24,
"mass": 1,
"velocity": 0,
"rippleSize": 220,
"rippleOpacity": 24,
"dragScale": 103,
"autoplay": false
};
const mount = (root, config) => {
const cleanups = [];
const addCleanup = (callback) => cleanups.push(callback);
const cleanup = () => {
for (let index = cleanups.length - 1; index >= 0; index -= 1) {
try { cleanups[index](); } catch { /* keep cleanup reentrant */ }
}
cleanups.length = 0;
};
const on = (target, type, listener, options) => {
target.addEventListener(type, listener, options);
addCleanup(() => target.removeEventListener(type, listener, options));
};
const reduced = () => root.closest(".force-reduced-motion") !== null ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const watchReduced = (callback) => {
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
const notify = () => callback(reduced());
media.addEventListener("change", notify);
const observer = typeof MutationObserver === "undefined" ? null : new MutationObserver(notify);
let ancestor = root;
while (observer && ancestor) {
observer.observe(ancestor, { attributes: true, attributeFilter: ["class"] });
ancestor = ancestor.parentElement;
}
addCleanup(() => {
media.removeEventListener("change", notify);
if (observer) observer.disconnect();
});
};
const replay = root.querySelector("[data-motion-replay]");
if (!replay) return cleanup;
const restart = () => {
root.getAnimations({ subtree: true }).forEach((animation) => {
animation.cancel();
animation.currentTime = 0;
animation.play();
});
};
addCleanup(() => root.getAnimations({ subtree: true }).forEach((animation) => {
if (animation.playState === "paused") animation.play();
}));
on(replay, "click", restart);
return cleanup;
};
document.querySelectorAll("[data-motion=\"number-ticker\"]").forEach((root) => {
const cleanupKey = "__motionLexiconCleanup";
if (typeof root[cleanupKey] === "function") root[cleanupKey]();
root[cleanupKey] = mount(root, config);
});
})();number-tickerNumeric glyphs translate or interpolate until the display reaches an updated value.
数字滚动或递增到目标值。
tabular-numbersTabular numeral glyphs share a fixed advance width, keeping changing values spatially stable.
使用固定宽度的数字,使数值变化时不会左右跳动;它对数字滚动、计时器和计数器非常重要。
Tabular numbers is a typographic feature that stabilizes digit width; Number ticker is the animation used when values change.
Replace the value immediately and indicate direction through static color or an arrow.
| Parameter | Range / options | Default | Purpose |
|---|---|---|---|
duration | 80ms–1200ms | 240ms | Controls the time required for one motion cycle. |
distance | 0px–60px | 24px | Controls how far the element travels. |
ease | Soft / Snap / Calm / Linear | Snap | Controls how velocity changes over time. |