Ticker examples.
Patterns to try, adjust and copy. Start with a live preview, then open the complete HTML / CDN or Package / ESM example below it.
Pause any preview to read at your own pace. Reduced motion and readable static content are built in.
News and updates
Share releases, notices and useful links in a moving news strip. Visitors can follow the original links without tabbing through repeated copies.
Hover or focus a link to pause. The pause button works independently of pointer hover.
Open code · HTML / CDN or Package / ESM
HTML / CDN opens directly as an index.html file with an internet connection; the CDN script exposes window.Ticker. For Package / ESM, install @samline/ticker and use the file in an HTML-entry bundler such as Vite. Both use the same ticker() API; a registry is optional.
Read or copy HTML, CSS and JavaScript as three files in the same folder. Use Complete file to copy one runnable index.html instead. Copied code uses the initial settings; preview changes are not included.
HTML / CDN
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>News and updates</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="News and updates">
<section class="ticker-recipe">
<h1>News and updates</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Ticker resources"
aria-live="off"
>
<div id="ticker-1" data-row>
<a
class="item"
href="https://samline.github.io/ticker/docs/getting-started/"
target="_blank"
rel="noopener noreferrer"
>Start with a working ticker</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/guides/accessibility/"
target="_blank"
rel="noopener noreferrer"
>Make motion accessible</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/reference/configuration/"
target="_blank"
rel="noopener noreferrer"
>Choose your configuration</a
>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>
Hover or focus a link to pause. The pause button works independently of pointer hover.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
const ticker = window.Ticker.ticker
const news = ticker('#ticker-1', { duration: 28, direction: 'left', pauseOnHover: true })
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>News and updates</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="News and updates">
<section class="ticker-recipe">
<h1>News and updates</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Ticker resources"
aria-live="off"
>
<div id="ticker-1" data-row>
<a
class="item"
href="https://samline.github.io/ticker/docs/getting-started/"
target="_blank"
rel="noopener noreferrer"
>Start with a working ticker</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/guides/accessibility/"
target="_blank"
rel="noopener noreferrer"
>Make motion accessible</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/reference/configuration/"
target="_blank"
rel="noopener noreferrer"
>Choose your configuration</a
>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>
Hover or focus a link to pause. The pause button works independently of pointer hover.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module">
const ticker = window.Ticker.ticker
const news = ticker('#ticker-1', { duration: 28, direction: 'left', pauseOnHover: true })
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
</script>
</body>
</html>
Package / ESM
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>News and updates</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="News and updates">
<section class="ticker-recipe">
<h1>News and updates</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Ticker resources"
aria-live="off"
>
<div id="ticker-1" data-row>
<a
class="item"
href="https://samline.github.io/ticker/docs/getting-started/"
target="_blank"
rel="noopener noreferrer"
>Start with a working ticker</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/guides/accessibility/"
target="_blank"
rel="noopener noreferrer"
>Make motion accessible</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/reference/configuration/"
target="_blank"
rel="noopener noreferrer"
>Choose your configuration</a
>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>
Hover or focus a link to pause. The pause button works independently of pointer hover.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const news = ticker('#ticker-1', { duration: 28, direction: 'left', pauseOnHover: true })
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>News and updates</title>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="News and updates">
<section class="ticker-recipe">
<h1>News and updates</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Ticker resources"
aria-live="off"
>
<div id="ticker-1" data-row>
<a
class="item"
href="https://samline.github.io/ticker/docs/getting-started/"
target="_blank"
rel="noopener noreferrer"
>Start with a working ticker</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/guides/accessibility/"
target="_blank"
rel="noopener noreferrer"
>Make motion accessible</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/reference/configuration/"
target="_blank"
rel="noopener noreferrer"
>Choose your configuration</a
>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>
Hover or focus a link to pause. The pause button works independently of pointer hover.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module">
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const news = ticker('#ticker-1', { duration: 28, direction: 'left', pauseOnHover: true })
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
</script>
</body>
</html>
Partners and tools
Give partners, sponsors or tools a place on your page. This example includes the SVGs in its copied code, ready to use with attribution.
Brand marks are illustrative and do not imply endorsement. Keep their attribution when reusing them.
Open code · HTML / CDN or Package / ESM
HTML / CDN opens directly as an index.html file with an internet connection; the CDN script exposes window.Ticker. For Package / ESM, install @samline/ticker and use the file in an HTML-entry bundler such as Vite. Both use the same ticker() API; a registry is optional.
Read or copy HTML, CSS and JavaScript as three files in the same folder. Use Complete file to copy one runnable index.html instead. Copied code uses the initial settings; preview changes are not included.
HTML / CDN
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Partners and tools</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="Partners and tools">
<section class="ticker-recipe">
<h1>Partners and tools</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Example technology marks"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EAstro%3C%2Ftitle%3E%3Cpath%20d%3D%22M8.358%2020.162c-1.186-1.07-1.532-3.316-1.038-4.944.856%201.026%202.043%201.352%203.272%201.535%201.897.283%203.76.177%205.522-.678.202-.098.388-.229.608-.36.166.473.209.95.151%201.437-.14%201.185-.738%202.1-1.688%202.794-.38.277-.782.525-1.175.787-1.205.804-1.531%201.747-1.078%203.119l.044.148a3.158%203.158%200%200%201-1.407-1.188%203.31%203.31%200%200%201-.544-1.815c-.004-.32-.004-.642-.048-.958-.106-.769-.472-1.113-1.161-1.133-.707-.02-1.267.411-1.415%201.09-.012.053-.028.104-.045.165h.002zm-5.961-4.445s3.24-1.575%206.49-1.575l2.451-7.565c.092-.366.36-.614.662-.614.302%200%20.57.248.662.614l2.45%207.565c3.85%200%206.491%201.575%206.491%201.575L16.088.727C15.93.285%2015.663%200%2015.303%200H8.697c-.36%200-.615.285-.784.727l-5.516%2014.99z%22%2F%3E%3C%2Fsvg%3E%0A"
alt=""
/>Astro</span
>
<span class="item"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EVite%3C%2Ftitle%3E%3Cpath%20d%3D%22m8.286%2010.578.512-8.657a.306.306%200%200%201%20.247-.282L17.377.006a.306.306%200%200%201%20.353.385l-1.558%205.403a.306.306%200%200%200%20.352.385l2.388-.46a.306.306%200%200%201%20.332.438l-6.79%2013.55-.123.19a.294.294%200%200%201-.252.14c-.177%200-.35-.152-.305-.369l1.095-5.301a.306.306%200%200%200-.388-.355l-1.433.435a.306.306%200%200%201-.389-.354l.69-3.375a.306.306%200%200%200-.37-.36l-2.32.536a.306.306%200%200%201-.374-.316zm14.976-7.926L17.284%203.74l-.544%201.887%202.077-.4a.8.8%200%200%201%20.84.369.8.8%200%200%201%20.034.783L12.9%2019.93l-.013.025-.015.023-.122.19a.801.801%200%200%201-.672.37.826.826%200%200%201-.634-.302.8.8%200%200%201-.16-.67l1.029-4.981-1.12.34a.81.81%200%200%201-.86-.262.802.802%200%200%201-.165-.67l.63-3.08-2.027.468a.808.808%200%200%201-.768-.233.81.81%200%200%201-.217-.6l.389-6.57-7.44-1.33a.612.612%200%200%200-.64.906L11.58%2023.691a.612.612%200%200%200%201.066-.004l11.26-20.135a.612.612%200%200%200-.644-.9z%22%2F%3E%3C%2Fsvg%3E%0A"
alt=""
/>Vite</span
>
<span class="item"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EGitHub%3C%2Ftitle%3E%3Cpath%20d%3D%22M12%20.297c-6.63%200-12%205.373-12%2012%200%205.303%203.438%209.8%208.205%2011.385.6.113.82-.258.82-.577%200-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422%2018.07%203.633%2017.7%203.633%2017.7c-1.087-.744.084-.729.084-.729%201.205.084%201.838%201.236%201.838%201.236%201.07%201.835%202.809%201.305%203.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93%200-1.31.465-2.38%201.235-3.22-.135-.303-.54-1.523.105-3.176%200%200%201.005-.322%203.3%201.23.96-.267%201.98-.399%203-.405%201.02.006%202.04.138%203%20.405%202.28-1.552%203.285-1.23%203.285-1.23.645%201.653.24%202.873.12%203.176.765.84%201.23%201.91%201.23%203.22%200%204.61-2.805%205.625-5.475%205.92.42.36.81%201.096.81%202.22%200%201.606-.015%202.896-.015%203.286%200%20.315.21.69.825.57C20.565%2022.092%2024%2017.592%2024%2012.297c0-6.627-5.373-12-12-12%22%2F%3E%3C%2Fsvg%3E%0A"
alt=""
/>GitHub</span
>
<span class="item"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EFigma%3C%2Ftitle%3E%3Cpath%20d%3D%22M15.852%208.981h-4.588V0h4.588c2.476%200%204.49%202.014%204.49%204.49s-2.014%204.491-4.49%204.491zM12.735%207.51h3.117c1.665%200%203.019-1.355%203.019-3.019s-1.355-3.019-3.019-3.019h-3.117V7.51zm0%201.471H8.148c-2.476%200-4.49-2.014-4.49-4.49S5.672%200%208.148%200h4.588v8.981zm-4.587-7.51c-1.665%200-3.019%201.355-3.019%203.019s1.354%203.02%203.019%203.02h3.117V1.471H8.148zm4.587%2015.019H8.148c-2.476%200-4.49-2.014-4.49-4.49s2.014-4.49%204.49-4.49h4.588v8.98zM8.148%208.981c-1.665%200-3.019%201.355-3.019%203.019s1.355%203.019%203.019%203.019h3.117V8.981H8.148zM8.172%2024c-2.489%200-4.515-2.014-4.515-4.49s2.014-4.49%204.49-4.49h4.588v4.441c0%202.503-2.047%204.539-4.563%204.539zm-.024-7.51a3.023%203.023%200%200%200-3.019%203.019c0%201.665%201.365%203.019%203.044%203.019%201.705%200%203.093-1.376%203.093-3.068v-2.97H8.148zm7.704%200h-.098c-2.476%200-4.49-2.014-4.49-4.49s2.014-4.49%204.49-4.49h.098c2.476%200%204.49%202.014%204.49%204.49s-2.014%204.49-4.49%204.49zm-.097-7.509c-1.665%200-3.019%201.355-3.019%203.019s1.355%203.019%203.019%203.019h.098c1.665%200%203.019-1.355%203.019-3.019s-1.355-3.019-3.019-3.019h-.098z%22%2F%3E%3C%2Fsvg%3E%0A"
alt=""
/>Figma</span
>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>
Brand marks are illustrative and do not imply endorsement. Keep their attribution when
reusing them.
</p>
<p>
<a
href="https://samline.github.io/ticker/attribution/"
target="_blank"
rel="noopener noreferrer"
>Demo brand marks and attribution</a
>
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
const ticker = window.Ticker.ticker
const news = ticker('#ticker-1', { duration: 36, direction: 'left', pauseOnHover: true })
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Partners and tools</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="Partners and tools">
<section class="ticker-recipe">
<h1>Partners and tools</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Example technology marks"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EAstro%3C%2Ftitle%3E%3Cpath%20d%3D%22M8.358%2020.162c-1.186-1.07-1.532-3.316-1.038-4.944.856%201.026%202.043%201.352%203.272%201.535%201.897.283%203.76.177%205.522-.678.202-.098.388-.229.608-.36.166.473.209.95.151%201.437-.14%201.185-.738%202.1-1.688%202.794-.38.277-.782.525-1.175.787-1.205.804-1.531%201.747-1.078%203.119l.044.148a3.158%203.158%200%200%201-1.407-1.188%203.31%203.31%200%200%201-.544-1.815c-.004-.32-.004-.642-.048-.958-.106-.769-.472-1.113-1.161-1.133-.707-.02-1.267.411-1.415%201.09-.012.053-.028.104-.045.165h.002zm-5.961-4.445s3.24-1.575%206.49-1.575l2.451-7.565c.092-.366.36-.614.662-.614.302%200%20.57.248.662.614l2.45%207.565c3.85%200%206.491%201.575%206.491%201.575L16.088.727C15.93.285%2015.663%200%2015.303%200H8.697c-.36%200-.615.285-.784.727l-5.516%2014.99z%22%2F%3E%3C%2Fsvg%3E%0A"
alt=""
/>Astro</span
>
<span class="item"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EVite%3C%2Ftitle%3E%3Cpath%20d%3D%22m8.286%2010.578.512-8.657a.306.306%200%200%201%20.247-.282L17.377.006a.306.306%200%200%201%20.353.385l-1.558%205.403a.306.306%200%200%200%20.352.385l2.388-.46a.306.306%200%200%201%20.332.438l-6.79%2013.55-.123.19a.294.294%200%200%201-.252.14c-.177%200-.35-.152-.305-.369l1.095-5.301a.306.306%200%200%200-.388-.355l-1.433.435a.306.306%200%200%201-.389-.354l.69-3.375a.306.306%200%200%200-.37-.36l-2.32.536a.306.306%200%200%201-.374-.316zm14.976-7.926L17.284%203.74l-.544%201.887%202.077-.4a.8.8%200%200%201%20.84.369.8.8%200%200%201%20.034.783L12.9%2019.93l-.013.025-.015.023-.122.19a.801.801%200%200%201-.672.37.826.826%200%200%201-.634-.302.8.8%200%200%201-.16-.67l1.029-4.981-1.12.34a.81.81%200%200%201-.86-.262.802.802%200%200%201-.165-.67l.63-3.08-2.027.468a.808.808%200%200%201-.768-.233.81.81%200%200%201-.217-.6l.389-6.57-7.44-1.33a.612.612%200%200%200-.64.906L11.58%2023.691a.612.612%200%200%200%201.066-.004l11.26-20.135a.612.612%200%200%200-.644-.9z%22%2F%3E%3C%2Fsvg%3E%0A"
alt=""
/>Vite</span
>
<span class="item"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EGitHub%3C%2Ftitle%3E%3Cpath%20d%3D%22M12%20.297c-6.63%200-12%205.373-12%2012%200%205.303%203.438%209.8%208.205%2011.385.6.113.82-.258.82-.577%200-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422%2018.07%203.633%2017.7%203.633%2017.7c-1.087-.744.084-.729.084-.729%201.205.084%201.838%201.236%201.838%201.236%201.07%201.835%202.809%201.305%203.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93%200-1.31.465-2.38%201.235-3.22-.135-.303-.54-1.523.105-3.176%200%200%201.005-.322%203.3%201.23.96-.267%201.98-.399%203-.405%201.02.006%202.04.138%203%20.405%202.28-1.552%203.285-1.23%203.285-1.23.645%201.653.24%202.873.12%203.176.765.84%201.23%201.91%201.23%203.22%200%204.61-2.805%205.625-5.475%205.92.42.36.81%201.096.81%202.22%200%201.606-.015%202.896-.015%203.286%200%20.315.21.69.825.57C20.565%2022.092%2024%2017.592%2024%2012.297c0-6.627-5.373-12-12-12%22%2F%3E%3C%2Fsvg%3E%0A"
alt=""
/>GitHub</span
>
<span class="item"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EFigma%3C%2Ftitle%3E%3Cpath%20d%3D%22M15.852%208.981h-4.588V0h4.588c2.476%200%204.49%202.014%204.49%204.49s-2.014%204.491-4.49%204.491zM12.735%207.51h3.117c1.665%200%203.019-1.355%203.019-3.019s-1.355-3.019-3.019-3.019h-3.117V7.51zm0%201.471H8.148c-2.476%200-4.49-2.014-4.49-4.49S5.672%200%208.148%200h4.588v8.981zm-4.587-7.51c-1.665%200-3.019%201.355-3.019%203.019s1.354%203.02%203.019%203.02h3.117V1.471H8.148zm4.587%2015.019H8.148c-2.476%200-4.49-2.014-4.49-4.49s2.014-4.49%204.49-4.49h4.588v8.98zM8.148%208.981c-1.665%200-3.019%201.355-3.019%203.019s1.355%203.019%203.019%203.019h3.117V8.981H8.148zM8.172%2024c-2.489%200-4.515-2.014-4.515-4.49s2.014-4.49%204.49-4.49h4.588v4.441c0%202.503-2.047%204.539-4.563%204.539zm-.024-7.51a3.023%203.023%200%200%200-3.019%203.019c0%201.665%201.365%203.019%203.044%203.019%201.705%200%203.093-1.376%203.093-3.068v-2.97H8.148zm7.704%200h-.098c-2.476%200-4.49-2.014-4.49-4.49s2.014-4.49%204.49-4.49h.098c2.476%200%204.49%202.014%204.49%204.49s-2.014%204.49-4.49%204.49zm-.097-7.509c-1.665%200-3.019%201.355-3.019%203.019s1.355%203.019%203.019%203.019h.098c1.665%200%203.019-1.355%203.019-3.019s-1.355-3.019-3.019-3.019h-.098z%22%2F%3E%3C%2Fsvg%3E%0A"
alt=""
/>Figma</span
>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>
Brand marks are illustrative and do not imply endorsement. Keep their attribution when
reusing them.
</p>
<p>
<a
href="https://samline.github.io/ticker/attribution/"
target="_blank"
rel="noopener noreferrer"
>Demo brand marks and attribution</a
>
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module">
const ticker = window.Ticker.ticker
const news = ticker('#ticker-1', { duration: 36, direction: 'left', pauseOnHover: true })
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
</script>
</body>
</html>
Package / ESM
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Partners and tools</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="Partners and tools">
<section class="ticker-recipe">
<h1>Partners and tools</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Example technology marks"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EAstro%3C%2Ftitle%3E%3Cpath%20d%3D%22M8.358%2020.162c-1.186-1.07-1.532-3.316-1.038-4.944.856%201.026%202.043%201.352%203.272%201.535%201.897.283%203.76.177%205.522-.678.202-.098.388-.229.608-.36.166.473.209.95.151%201.437-.14%201.185-.738%202.1-1.688%202.794-.38.277-.782.525-1.175.787-1.205.804-1.531%201.747-1.078%203.119l.044.148a3.158%203.158%200%200%201-1.407-1.188%203.31%203.31%200%200%201-.544-1.815c-.004-.32-.004-.642-.048-.958-.106-.769-.472-1.113-1.161-1.133-.707-.02-1.267.411-1.415%201.09-.012.053-.028.104-.045.165h.002zm-5.961-4.445s3.24-1.575%206.49-1.575l2.451-7.565c.092-.366.36-.614.662-.614.302%200%20.57.248.662.614l2.45%207.565c3.85%200%206.491%201.575%206.491%201.575L16.088.727C15.93.285%2015.663%200%2015.303%200H8.697c-.36%200-.615.285-.784.727l-5.516%2014.99z%22%2F%3E%3C%2Fsvg%3E%0A"
alt=""
/>Astro</span
>
<span class="item"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EVite%3C%2Ftitle%3E%3Cpath%20d%3D%22m8.286%2010.578.512-8.657a.306.306%200%200%201%20.247-.282L17.377.006a.306.306%200%200%201%20.353.385l-1.558%205.403a.306.306%200%200%200%20.352.385l2.388-.46a.306.306%200%200%201%20.332.438l-6.79%2013.55-.123.19a.294.294%200%200%201-.252.14c-.177%200-.35-.152-.305-.369l1.095-5.301a.306.306%200%200%200-.388-.355l-1.433.435a.306.306%200%200%201-.389-.354l.69-3.375a.306.306%200%200%200-.37-.36l-2.32.536a.306.306%200%200%201-.374-.316zm14.976-7.926L17.284%203.74l-.544%201.887%202.077-.4a.8.8%200%200%201%20.84.369.8.8%200%200%201%20.034.783L12.9%2019.93l-.013.025-.015.023-.122.19a.801.801%200%200%201-.672.37.826.826%200%200%201-.634-.302.8.8%200%200%201-.16-.67l1.029-4.981-1.12.34a.81.81%200%200%201-.86-.262.802.802%200%200%201-.165-.67l.63-3.08-2.027.468a.808.808%200%200%201-.768-.233.81.81%200%200%201-.217-.6l.389-6.57-7.44-1.33a.612.612%200%200%200-.64.906L11.58%2023.691a.612.612%200%200%200%201.066-.004l11.26-20.135a.612.612%200%200%200-.644-.9z%22%2F%3E%3C%2Fsvg%3E%0A"
alt=""
/>Vite</span
>
<span class="item"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EGitHub%3C%2Ftitle%3E%3Cpath%20d%3D%22M12%20.297c-6.63%200-12%205.373-12%2012%200%205.303%203.438%209.8%208.205%2011.385.6.113.82-.258.82-.577%200-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422%2018.07%203.633%2017.7%203.633%2017.7c-1.087-.744.084-.729.084-.729%201.205.084%201.838%201.236%201.838%201.236%201.07%201.835%202.809%201.305%203.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93%200-1.31.465-2.38%201.235-3.22-.135-.303-.54-1.523.105-3.176%200%200%201.005-.322%203.3%201.23.96-.267%201.98-.399%203-.405%201.02.006%202.04.138%203%20.405%202.28-1.552%203.285-1.23%203.285-1.23.645%201.653.24%202.873.12%203.176.765.84%201.23%201.91%201.23%203.22%200%204.61-2.805%205.625-5.475%205.92.42.36.81%201.096.81%202.22%200%201.606-.015%202.896-.015%203.286%200%20.315.21.69.825.57C20.565%2022.092%2024%2017.592%2024%2012.297c0-6.627-5.373-12-12-12%22%2F%3E%3C%2Fsvg%3E%0A"
alt=""
/>GitHub</span
>
<span class="item"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EFigma%3C%2Ftitle%3E%3Cpath%20d%3D%22M15.852%208.981h-4.588V0h4.588c2.476%200%204.49%202.014%204.49%204.49s-2.014%204.491-4.49%204.491zM12.735%207.51h3.117c1.665%200%203.019-1.355%203.019-3.019s-1.355-3.019-3.019-3.019h-3.117V7.51zm0%201.471H8.148c-2.476%200-4.49-2.014-4.49-4.49S5.672%200%208.148%200h4.588v8.981zm-4.587-7.51c-1.665%200-3.019%201.355-3.019%203.019s1.354%203.02%203.019%203.02h3.117V1.471H8.148zm4.587%2015.019H8.148c-2.476%200-4.49-2.014-4.49-4.49s2.014-4.49%204.49-4.49h4.588v8.98zM8.148%208.981c-1.665%200-3.019%201.355-3.019%203.019s1.355%203.019%203.019%203.019h3.117V8.981H8.148zM8.172%2024c-2.489%200-4.515-2.014-4.515-4.49s2.014-4.49%204.49-4.49h4.588v4.441c0%202.503-2.047%204.539-4.563%204.539zm-.024-7.51a3.023%203.023%200%200%200-3.019%203.019c0%201.665%201.365%203.019%203.044%203.019%201.705%200%203.093-1.376%203.093-3.068v-2.97H8.148zm7.704%200h-.098c-2.476%200-4.49-2.014-4.49-4.49s2.014-4.49%204.49-4.49h.098c2.476%200%204.49%202.014%204.49%204.49s-2.014%204.49-4.49%204.49zm-.097-7.509c-1.665%200-3.019%201.355-3.019%203.019s1.355%203.019%203.019%203.019h.098c1.665%200%203.019-1.355%203.019-3.019s-1.355-3.019-3.019-3.019h-.098z%22%2F%3E%3C%2Fsvg%3E%0A"
alt=""
/>Figma</span
>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>
Brand marks are illustrative and do not imply endorsement. Keep their attribution when
reusing them.
</p>
<p>
<a
href="https://samline.github.io/ticker/attribution/"
target="_blank"
rel="noopener noreferrer"
>Demo brand marks and attribution</a
>
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const news = ticker('#ticker-1', { duration: 36, direction: 'left', pauseOnHover: true })
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Partners and tools</title>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="Partners and tools">
<section class="ticker-recipe">
<h1>Partners and tools</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Example technology marks"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EAstro%3C%2Ftitle%3E%3Cpath%20d%3D%22M8.358%2020.162c-1.186-1.07-1.532-3.316-1.038-4.944.856%201.026%202.043%201.352%203.272%201.535%201.897.283%203.76.177%205.522-.678.202-.098.388-.229.608-.36.166.473.209.95.151%201.437-.14%201.185-.738%202.1-1.688%202.794-.38.277-.782.525-1.175.787-1.205.804-1.531%201.747-1.078%203.119l.044.148a3.158%203.158%200%200%201-1.407-1.188%203.31%203.31%200%200%201-.544-1.815c-.004-.32-.004-.642-.048-.958-.106-.769-.472-1.113-1.161-1.133-.707-.02-1.267.411-1.415%201.09-.012.053-.028.104-.045.165h.002zm-5.961-4.445s3.24-1.575%206.49-1.575l2.451-7.565c.092-.366.36-.614.662-.614.302%200%20.57.248.662.614l2.45%207.565c3.85%200%206.491%201.575%206.491%201.575L16.088.727C15.93.285%2015.663%200%2015.303%200H8.697c-.36%200-.615.285-.784.727l-5.516%2014.99z%22%2F%3E%3C%2Fsvg%3E%0A"
alt=""
/>Astro</span
>
<span class="item"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EVite%3C%2Ftitle%3E%3Cpath%20d%3D%22m8.286%2010.578.512-8.657a.306.306%200%200%201%20.247-.282L17.377.006a.306.306%200%200%201%20.353.385l-1.558%205.403a.306.306%200%200%200%20.352.385l2.388-.46a.306.306%200%200%201%20.332.438l-6.79%2013.55-.123.19a.294.294%200%200%201-.252.14c-.177%200-.35-.152-.305-.369l1.095-5.301a.306.306%200%200%200-.388-.355l-1.433.435a.306.306%200%200%201-.389-.354l.69-3.375a.306.306%200%200%200-.37-.36l-2.32.536a.306.306%200%200%201-.374-.316zm14.976-7.926L17.284%203.74l-.544%201.887%202.077-.4a.8.8%200%200%201%20.84.369.8.8%200%200%201%20.034.783L12.9%2019.93l-.013.025-.015.023-.122.19a.801.801%200%200%201-.672.37.826.826%200%200%201-.634-.302.8.8%200%200%201-.16-.67l1.029-4.981-1.12.34a.81.81%200%200%201-.86-.262.802.802%200%200%201-.165-.67l.63-3.08-2.027.468a.808.808%200%200%201-.768-.233.81.81%200%200%201-.217-.6l.389-6.57-7.44-1.33a.612.612%200%200%200-.64.906L11.58%2023.691a.612.612%200%200%200%201.066-.004l11.26-20.135a.612.612%200%200%200-.644-.9z%22%2F%3E%3C%2Fsvg%3E%0A"
alt=""
/>Vite</span
>
<span class="item"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EGitHub%3C%2Ftitle%3E%3Cpath%20d%3D%22M12%20.297c-6.63%200-12%205.373-12%2012%200%205.303%203.438%209.8%208.205%2011.385.6.113.82-.258.82-.577%200-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422%2018.07%203.633%2017.7%203.633%2017.7c-1.087-.744.084-.729.084-.729%201.205.084%201.838%201.236%201.838%201.236%201.07%201.835%202.809%201.305%203.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93%200-1.31.465-2.38%201.235-3.22-.135-.303-.54-1.523.105-3.176%200%200%201.005-.322%203.3%201.23.96-.267%201.98-.399%203-.405%201.02.006%202.04.138%203%20.405%202.28-1.552%203.285-1.23%203.285-1.23.645%201.653.24%202.873.12%203.176.765.84%201.23%201.91%201.23%203.22%200%204.61-2.805%205.625-5.475%205.92.42.36.81%201.096.81%202.22%200%201.606-.015%202.896-.015%203.286%200%20.315.21.69.825.57C20.565%2022.092%2024%2017.592%2024%2012.297c0-6.627-5.373-12-12-12%22%2F%3E%3C%2Fsvg%3E%0A"
alt=""
/>GitHub</span
>
<span class="item"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EFigma%3C%2Ftitle%3E%3Cpath%20d%3D%22M15.852%208.981h-4.588V0h4.588c2.476%200%204.49%202.014%204.49%204.49s-2.014%204.491-4.49%204.491zM12.735%207.51h3.117c1.665%200%203.019-1.355%203.019-3.019s-1.355-3.019-3.019-3.019h-3.117V7.51zm0%201.471H8.148c-2.476%200-4.49-2.014-4.49-4.49S5.672%200%208.148%200h4.588v8.981zm-4.587-7.51c-1.665%200-3.019%201.355-3.019%203.019s1.354%203.02%203.019%203.02h3.117V1.471H8.148zm4.587%2015.019H8.148c-2.476%200-4.49-2.014-4.49-4.49s2.014-4.49%204.49-4.49h4.588v8.98zM8.148%208.981c-1.665%200-3.019%201.355-3.019%203.019s1.355%203.019%203.019%203.019h3.117V8.981H8.148zM8.172%2024c-2.489%200-4.515-2.014-4.515-4.49s2.014-4.49%204.49-4.49h4.588v4.441c0%202.503-2.047%204.539-4.563%204.539zm-.024-7.51a3.023%203.023%200%200%200-3.019%203.019c0%201.665%201.365%203.019%203.044%203.019%201.705%200%203.093-1.376%203.093-3.068v-2.97H8.148zm7.704%200h-.098c-2.476%200-4.49-2.014-4.49-4.49s2.014-4.49%204.49-4.49h.098c2.476%200%204.49%202.014%204.49%204.49s-2.014%204.49-4.49%204.49zm-.097-7.509c-1.665%200-3.019%201.355-3.019%203.019s1.355%203.019%203.019%203.019h.098c1.665%200%203.019-1.355%203.019-3.019s-1.355-3.019-3.019-3.019h-.098z%22%2F%3E%3C%2Fsvg%3E%0A"
alt=""
/>Figma</span
>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>
Brand marks are illustrative and do not imply endorsement. Keep their attribution when
reusing them.
</p>
<p>
<a
href="https://samline.github.io/ticker/attribution/"
target="_blank"
rel="noopener noreferrer"
>Demo brand marks and attribution</a
>
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module">
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const news = ticker('#ticker-1', { duration: 36, direction: 'left', pauseOnHover: true })
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
</script>
</body>
</html>
Prices and metrics
Display prices, scores or other changing numbers in a compact row. Plus and minus signs make the trend clear alongside color.
Illustrative data only. These values do not update from a market feed.
Open code · HTML / CDN or Package / ESM
HTML / CDN opens directly as an index.html file with an internet connection; the CDN script exposes window.Ticker. For Package / ESM, install @samline/ticker and use the file in an HTML-entry bundler such as Vite. Both use the same ticker() API; a registry is optional.
Read or copy HTML, CSS and JavaScript as three files in the same folder. Use Complete file to copy one runnable index.html instead. Copied code uses the initial settings; preview changes are not included.
HTML / CDN
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Prices and metrics</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="Prices and metrics">
<section class="ticker-recipe">
<h1>Prices and metrics</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Illustrative market data"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item"><strong>ACME</strong> $128.40 <span class="up">+2.4%</span></span>
<span class="item"><strong>ORBIT</strong> $84.12 <span class="down">−0.8%</span></span>
<span class="item"><strong>LAYER</strong> $206.55 <span class="up">+1.3%</span></span>
<span class="item"><strong>NORTH</strong> $62.70 <span class="up">+0.6%</span></span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>Illustrative data only. These values do not update from a market feed.</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
const ticker = window.Ticker.ticker
const news = ticker('#ticker-1', { duration: 32, direction: 'right', pauseOnHover: true })
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Prices and metrics</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="Prices and metrics">
<section class="ticker-recipe">
<h1>Prices and metrics</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Illustrative market data"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item"><strong>ACME</strong> $128.40 <span class="up">+2.4%</span></span>
<span class="item"><strong>ORBIT</strong> $84.12 <span class="down">−0.8%</span></span>
<span class="item"><strong>LAYER</strong> $206.55 <span class="up">+1.3%</span></span>
<span class="item"><strong>NORTH</strong> $62.70 <span class="up">+0.6%</span></span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>Illustrative data only. These values do not update from a market feed.</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module">
const ticker = window.Ticker.ticker
const news = ticker('#ticker-1', { duration: 32, direction: 'right', pauseOnHover: true })
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
</script>
</body>
</html>
Package / ESM
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Prices and metrics</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="Prices and metrics">
<section class="ticker-recipe">
<h1>Prices and metrics</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Illustrative market data"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item"><strong>ACME</strong> $128.40 <span class="up">+2.4%</span></span>
<span class="item"><strong>ORBIT</strong> $84.12 <span class="down">−0.8%</span></span>
<span class="item"><strong>LAYER</strong> $206.55 <span class="up">+1.3%</span></span>
<span class="item"><strong>NORTH</strong> $62.70 <span class="up">+0.6%</span></span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>Illustrative data only. These values do not update from a market feed.</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const news = ticker('#ticker-1', { duration: 32, direction: 'right', pauseOnHover: true })
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Prices and metrics</title>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="Prices and metrics">
<section class="ticker-recipe">
<h1>Prices and metrics</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Illustrative market data"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item"><strong>ACME</strong> $128.40 <span class="up">+2.4%</span></span>
<span class="item"><strong>ORBIT</strong> $84.12 <span class="down">−0.8%</span></span>
<span class="item"><strong>LAYER</strong> $206.55 <span class="up">+1.3%</span></span>
<span class="item"><strong>NORTH</strong> $62.70 <span class="up">+0.6%</span></span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>Illustrative data only. These values do not update from a market feed.</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module">
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const news = ticker('#ticker-1', { duration: 32, direction: 'right', pauseOnHover: true })
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
</script>
</body>
</html>
Two rows, two directions
Pair two strips moving in opposite directions, each at its own pace. In the preview, you can pause either row without stopping the other.
Each row is a separate instance. They share content styling, not runtime state. The standalone example includes a combined pause control.
Open code · HTML / CDN or Package / ESM
HTML / CDN opens directly as an index.html file with an internet connection; the CDN script exposes window.Ticker. For Package / ESM, install @samline/ticker and use the file in an HTML-entry bundler such as Vite. Both use the same ticker() API; a registry is optional.
Read or copy HTML, CSS and JavaScript as three files in the same folder. Use Complete file to copy one runnable index.html instead. Copied code uses the initial settings; preview changes are not included.
HTML / CDN
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Two rows, two directions</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="Two rows, two directions">
<section class="ticker-recipe">
<h1>Two rows, two directions</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Creative disciplines"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item">Design</span>
<span class="item">Development</span>
<span class="item">Motion</span>
<span class="item">Accessibility</span>
</div>
</div>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Ways of working"
aria-live="off"
>
<div id="ticker-2" data-row>
<span class="item">Build with care</span>
<span class="item">Keep it readable</span>
<span class="item">Ship something useful</span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>
Each row is a separate instance. They share content styling, not runtime state. The
standalone example includes a combined pause control.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
const ticker = window.Ticker.ticker
const tickers = [
ticker('#ticker-1', { duration: 26, direction: 'left', pauseOnHover: true }),
ticker('#ticker-2', { duration: 38, direction: 'right', pauseOnHover: true }),
].filter(Boolean)
const news = tickers[0]
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = tickers.some(item => item.state.requested)
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
tickers.forEach(news => {
news.on('playback', renderPlayback)
})
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = tickers.some(item => item.state.requested)
tickers.forEach(news => {
playing ? news.pause() : news.play()
})
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
tickers.forEach(news => {
news.refresh()
})
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
tickers.forEach(news => {
news.destroy()
})
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Two rows, two directions</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="Two rows, two directions">
<section class="ticker-recipe">
<h1>Two rows, two directions</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Creative disciplines"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item">Design</span>
<span class="item">Development</span>
<span class="item">Motion</span>
<span class="item">Accessibility</span>
</div>
</div>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Ways of working"
aria-live="off"
>
<div id="ticker-2" data-row>
<span class="item">Build with care</span>
<span class="item">Keep it readable</span>
<span class="item">Ship something useful</span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>
Each row is a separate instance. They share content styling, not runtime state. The
standalone example includes a combined pause control.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module">
const ticker = window.Ticker.ticker
const tickers = [
ticker('#ticker-1', { duration: 26, direction: 'left', pauseOnHover: true }),
ticker('#ticker-2', { duration: 38, direction: 'right', pauseOnHover: true }),
].filter(Boolean)
const news = tickers[0]
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = tickers.some(item => item.state.requested)
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
tickers.forEach(news => {
news.on('playback', renderPlayback)
})
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = tickers.some(item => item.state.requested)
tickers.forEach(news => {
playing ? news.pause() : news.play()
})
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
tickers.forEach(news => {
news.refresh()
})
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
tickers.forEach(news => {
news.destroy()
})
},
{ signal }
)
}
</script>
</body>
</html>
Package / ESM
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Two rows, two directions</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="Two rows, two directions">
<section class="ticker-recipe">
<h1>Two rows, two directions</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Creative disciplines"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item">Design</span>
<span class="item">Development</span>
<span class="item">Motion</span>
<span class="item">Accessibility</span>
</div>
</div>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Ways of working"
aria-live="off"
>
<div id="ticker-2" data-row>
<span class="item">Build with care</span>
<span class="item">Keep it readable</span>
<span class="item">Ship something useful</span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>
Each row is a separate instance. They share content styling, not runtime state. The
standalone example includes a combined pause control.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const tickers = [
ticker('#ticker-1', { duration: 26, direction: 'left', pauseOnHover: true }),
ticker('#ticker-2', { duration: 38, direction: 'right', pauseOnHover: true }),
].filter(Boolean)
const news = tickers[0]
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = tickers.some(item => item.state.requested)
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
tickers.forEach(news => {
news.on('playback', renderPlayback)
})
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = tickers.some(item => item.state.requested)
tickers.forEach(news => {
playing ? news.pause() : news.play()
})
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
tickers.forEach(news => {
news.refresh()
})
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
tickers.forEach(news => {
news.destroy()
})
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Two rows, two directions</title>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="Two rows, two directions">
<section class="ticker-recipe">
<h1>Two rows, two directions</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Creative disciplines"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item">Design</span>
<span class="item">Development</span>
<span class="item">Motion</span>
<span class="item">Accessibility</span>
</div>
</div>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Ways of working"
aria-live="off"
>
<div id="ticker-2" data-row>
<span class="item">Build with care</span>
<span class="item">Keep it readable</span>
<span class="item">Ship something useful</span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>
Each row is a separate instance. They share content styling, not runtime state. The
standalone example includes a combined pause control.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module">
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const tickers = [
ticker('#ticker-1', { duration: 26, direction: 'left', pauseOnHover: true }),
ticker('#ticker-2', { duration: 38, direction: 'right', pauseOnHover: true }),
].filter(Boolean)
const news = tickers[0]
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = tickers.some(item => item.state.requested)
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
tickers.forEach(news => {
news.on('playback', renderPlayback)
})
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = tickers.some(item => item.state.requested)
tickers.forEach(news => {
playing ? news.pause() : news.play()
})
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
tickers.forEach(news => {
news.refresh()
})
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
tickers.forEach(news => {
news.destroy()
})
},
{ signal }
)
}
</script>
</body>
</html>
A feed that grows
Add or remove announcements while the feed keeps a constant 40 pixels per second. Try removing the first, last or selected item; the original content and its visual copies stay in sync.
The application owns the items and calls refresh() after editing the source. Pixel speed keeps the pace steady as the feed grows or shrinks.
Open code · HTML / CDN or Package / ESM
HTML / CDN opens directly as an index.html file with an internet connection; the CDN script exposes window.Ticker. For Package / ESM, install @samline/ticker and use the file in an HTML-entry bundler such as Vite. Both use the same ticker() API; a registry is optional.
Read or copy HTML, CSS and JavaScript as three files in the same folder. Use Complete file to copy one runnable index.html instead. Copied code uses the initial settings; preview changes are not included.
HTML / CDN
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>A feed that grows</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="A feed that grows">
<section class="ticker-recipe">
<h1>A feed that grows</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Live example updates"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item" data-example-item="item-0">Project created</span>
<span class="item" data-example-item="item-1">Design approved</span>
<span class="item" data-example-item="item-2">Ready to ship</span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
<button type="button" data-add>Add an update</button>
<button type="button" data-remove-first>Remove first</button>
<button type="button" data-remove-last>Remove last</button>
<label
>Choose an item
<select data-item-select></select
></label>
<button type="button" data-remove-selected>Remove selected</button>
</div>
<p>
The application owns the items and calls refresh() after editing the source. Pixel speed
keeps the pace steady as the feed grows or shrinks.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
const ticker = window.Ticker.ticker
const news = ticker('#ticker-1', { duration: 28, speed: 40, direction: 'left', pauseOnHover: true })
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
const select = example.querySelector('[data-item-select]')
const status = example.querySelector('[role=status]')
const items = () => [...news.source.querySelectorAll('[data-example-item]')]
const removeButtons = example.querySelectorAll(
'[data-remove-first], [data-remove-last], [data-remove-selected]'
)
const syncItems = () => {
const selected = select.value
const current = items()
select.replaceChildren(
...current.map(item => new Option(item.textContent, item.dataset.exampleItem))
)
if (current.some(item => item.dataset.exampleItem === selected)) select.value = selected
if (!current.length) select.add(new Option('No items left', ''))
select.disabled = !current.length
removeButtons.forEach(button => {
button.disabled = !current.length
})
}
const removeItem = item => {
if (!item) return
const label = item.textContent
item.remove()
news.refresh()
syncItems()
status.textContent = 'Removed {item}. Visual copies refreshed.'.replace('{item}', label)
}
example
.querySelector('[data-remove-first]')
.addEventListener('click', () => removeItem(items()[0]), { signal })
example
.querySelector('[data-remove-last]')
.addEventListener('click', () => removeItem(items().at(-1)), { signal })
example.querySelector('[data-remove-selected]').addEventListener(
'click',
() => {
removeItem(items().find(item => item.dataset.exampleItem === select.value))
},
{ signal }
)
syncItems()
let count = 0
example.querySelector('[data-add]').addEventListener(
'click',
() => {
const item = document.createElement('span')
item.className = 'item'
item.dataset.exampleItem = 'added-' + ++count
item.textContent = 'New update {count}'.replace('{count}', String(count))
news.source.append(item)
news.refresh()
syncItems()
status.textContent = 'Added update {count}. Visual copies refreshed.'.replace(
'{count}',
String(count)
)
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>A feed that grows</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="A feed that grows">
<section class="ticker-recipe">
<h1>A feed that grows</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Live example updates"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item" data-example-item="item-0">Project created</span>
<span class="item" data-example-item="item-1">Design approved</span>
<span class="item" data-example-item="item-2">Ready to ship</span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
<button type="button" data-add>Add an update</button>
<button type="button" data-remove-first>Remove first</button>
<button type="button" data-remove-last>Remove last</button>
<label
>Choose an item
<select data-item-select></select
></label>
<button type="button" data-remove-selected>Remove selected</button>
</div>
<p>
The application owns the items and calls refresh() after editing the source. Pixel speed
keeps the pace steady as the feed grows or shrinks.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module">
const ticker = window.Ticker.ticker
const news = ticker('#ticker-1', {
duration: 28,
speed: 40,
direction: 'left',
pauseOnHover: true,
})
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
const select = example.querySelector('[data-item-select]')
const status = example.querySelector('[role=status]')
const items = () => [...news.source.querySelectorAll('[data-example-item]')]
const removeButtons = example.querySelectorAll(
'[data-remove-first], [data-remove-last], [data-remove-selected]'
)
const syncItems = () => {
const selected = select.value
const current = items()
select.replaceChildren(
...current.map(item => new Option(item.textContent, item.dataset.exampleItem))
)
if (current.some(item => item.dataset.exampleItem === selected)) select.value = selected
if (!current.length) select.add(new Option('No items left', ''))
select.disabled = !current.length
removeButtons.forEach(button => {
button.disabled = !current.length
})
}
const removeItem = item => {
if (!item) return
const label = item.textContent
item.remove()
news.refresh()
syncItems()
status.textContent = 'Removed {item}. Visual copies refreshed.'.replace('{item}', label)
}
example
.querySelector('[data-remove-first]')
.addEventListener('click', () => removeItem(items()[0]), { signal })
example
.querySelector('[data-remove-last]')
.addEventListener('click', () => removeItem(items().at(-1)), { signal })
example.querySelector('[data-remove-selected]').addEventListener(
'click',
() => {
removeItem(items().find(item => item.dataset.exampleItem === select.value))
},
{ signal }
)
syncItems()
let count = 0
example.querySelector('[data-add]').addEventListener(
'click',
() => {
const item = document.createElement('span')
item.className = 'item'
item.dataset.exampleItem = 'added-' + ++count
item.textContent = 'New update {count}'.replace('{count}', String(count))
news.source.append(item)
news.refresh()
syncItems()
status.textContent = 'Added update {count}. Visual copies refreshed.'.replace(
'{count}',
String(count)
)
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
</script>
</body>
</html>
Package / ESM
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>A feed that grows</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="A feed that grows">
<section class="ticker-recipe">
<h1>A feed that grows</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Live example updates"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item" data-example-item="item-0">Project created</span>
<span class="item" data-example-item="item-1">Design approved</span>
<span class="item" data-example-item="item-2">Ready to ship</span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
<button type="button" data-add>Add an update</button>
<button type="button" data-remove-first>Remove first</button>
<button type="button" data-remove-last>Remove last</button>
<label
>Choose an item
<select data-item-select></select
></label>
<button type="button" data-remove-selected>Remove selected</button>
</div>
<p>
The application owns the items and calls refresh() after editing the source. Pixel speed
keeps the pace steady as the feed grows or shrinks.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const news = ticker('#ticker-1', { duration: 28, speed: 40, direction: 'left', pauseOnHover: true })
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
const select = example.querySelector('[data-item-select]')
const status = example.querySelector('[role=status]')
const items = () => [...news.source.querySelectorAll('[data-example-item]')]
const removeButtons = example.querySelectorAll(
'[data-remove-first], [data-remove-last], [data-remove-selected]'
)
const syncItems = () => {
const selected = select.value
const current = items()
select.replaceChildren(
...current.map(item => new Option(item.textContent, item.dataset.exampleItem))
)
if (current.some(item => item.dataset.exampleItem === selected)) select.value = selected
if (!current.length) select.add(new Option('No items left', ''))
select.disabled = !current.length
removeButtons.forEach(button => {
button.disabled = !current.length
})
}
const removeItem = item => {
if (!item) return
const label = item.textContent
item.remove()
news.refresh()
syncItems()
status.textContent = 'Removed {item}. Visual copies refreshed.'.replace('{item}', label)
}
example
.querySelector('[data-remove-first]')
.addEventListener('click', () => removeItem(items()[0]), { signal })
example
.querySelector('[data-remove-last]')
.addEventListener('click', () => removeItem(items().at(-1)), { signal })
example.querySelector('[data-remove-selected]').addEventListener(
'click',
() => {
removeItem(items().find(item => item.dataset.exampleItem === select.value))
},
{ signal }
)
syncItems()
let count = 0
example.querySelector('[data-add]').addEventListener(
'click',
() => {
const item = document.createElement('span')
item.className = 'item'
item.dataset.exampleItem = 'added-' + ++count
item.textContent = 'New update {count}'.replace('{count}', String(count))
news.source.append(item)
news.refresh()
syncItems()
status.textContent = 'Added update {count}. Visual copies refreshed.'.replace(
'{count}',
String(count)
)
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>A feed that grows</title>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="A feed that grows">
<section class="ticker-recipe">
<h1>A feed that grows</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Live example updates"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item" data-example-item="item-0">Project created</span>
<span class="item" data-example-item="item-1">Design approved</span>
<span class="item" data-example-item="item-2">Ready to ship</span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
<button type="button" data-add>Add an update</button>
<button type="button" data-remove-first>Remove first</button>
<button type="button" data-remove-last>Remove last</button>
<label
>Choose an item
<select data-item-select></select
></label>
<button type="button" data-remove-selected>Remove selected</button>
</div>
<p>
The application owns the items and calls refresh() after editing the source. Pixel speed
keeps the pace steady as the feed grows or shrinks.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module">
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const news = ticker('#ticker-1', {
duration: 28,
speed: 40,
direction: 'left',
pauseOnHover: true,
})
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
const select = example.querySelector('[data-item-select]')
const status = example.querySelector('[role=status]')
const items = () => [...news.source.querySelectorAll('[data-example-item]')]
const removeButtons = example.querySelectorAll(
'[data-remove-first], [data-remove-last], [data-remove-selected]'
)
const syncItems = () => {
const selected = select.value
const current = items()
select.replaceChildren(
...current.map(item => new Option(item.textContent, item.dataset.exampleItem))
)
if (current.some(item => item.dataset.exampleItem === selected)) select.value = selected
if (!current.length) select.add(new Option('No items left', ''))
select.disabled = !current.length
removeButtons.forEach(button => {
button.disabled = !current.length
})
}
const removeItem = item => {
if (!item) return
const label = item.textContent
item.remove()
news.refresh()
syncItems()
status.textContent = 'Removed {item}. Visual copies refreshed.'.replace('{item}', label)
}
example
.querySelector('[data-remove-first]')
.addEventListener('click', () => removeItem(items()[0]), { signal })
example
.querySelector('[data-remove-last]')
.addEventListener('click', () => removeItem(items().at(-1)), { signal })
example.querySelector('[data-remove-selected]').addEventListener(
'click',
() => {
removeItem(items().find(item => item.dataset.exampleItem === select.value))
},
{ signal }
)
syncItems()
let count = 0
example.querySelector('[data-add]').addEventListener(
'click',
() => {
const item = document.createElement('span')
item.className = 'item'
item.dataset.exampleItem = 'added-' + ++count
item.textContent = 'New update {count}'.replace('{count}', String(count))
news.source.append(item)
news.refresh()
syncItems()
status.textContent = 'Added update {count}. Visual copies refreshed.'.replace(
'{count}',
String(count)
)
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
</script>
</body>
</html>
Speed and direction
Find a pace that suits your content, then try the opposite direction. A shorter loop moves faster; your content stays in place while the settings change.
A shorter duration moves faster. Focus inside the preview pauses motion so controls and content stay usable.
Open code · HTML / CDN or Package / ESM
HTML / CDN opens directly as an index.html file with an internet connection; the CDN script exposes window.Ticker. For Package / ESM, install @samline/ticker and use the file in an HTML-entry bundler such as Vite. Both use the same ticker() API; a registry is optional.
Read or copy HTML, CSS and JavaScript as three files in the same folder. Use Complete file to copy one runnable index.html instead. Copied code uses the initial settings; preview changes are not included.
HTML / CDN
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Speed and direction</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="Speed and direction">
<section class="ticker-recipe">
<h1>Speed and direction</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Adjustable ticker"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item">Your content</span>
<span class="item">Your direction</span>
<span class="item">Your pace</span>
<span class="item">One small library</span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
<label
>Loop duration (seconds)
<input
type="range"
min="14"
max="60"
value="24"
aria-label="Loop duration (seconds)"
/><output>24s</output></label
>
<label
>Direction
<select data-example-direction-select aria-label="Direction">
<option value="left">Left</option>
<option value="right">Right</option>
</select></label
>
</div>
<p>
A shorter duration moves faster. Focus inside the preview pauses motion so controls and
content stay usable.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
const ticker = window.Ticker.ticker
const news = ticker('#ticker-1', { duration: 24, direction: 'left', pauseOnHover: true })
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
const duration = example.querySelector('input')
const output = example.querySelector('output')
duration.addEventListener(
'input',
() => {
news.update({ duration: Number(duration.value) })
output.value = duration.value + 's'
},
{ signal }
)
const direction = example.querySelector('[data-example-direction-select]')
direction.addEventListener(
'change',
() => {
news.update({ direction: direction.value })
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Speed and direction</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="Speed and direction">
<section class="ticker-recipe">
<h1>Speed and direction</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Adjustable ticker"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item">Your content</span>
<span class="item">Your direction</span>
<span class="item">Your pace</span>
<span class="item">One small library</span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
<label
>Loop duration (seconds)
<input
type="range"
min="14"
max="60"
value="24"
aria-label="Loop duration (seconds)"
/><output>24s</output></label
>
<label
>Direction
<select data-example-direction-select aria-label="Direction">
<option value="left">Left</option>
<option value="right">Right</option>
</select></label
>
</div>
<p>
A shorter duration moves faster. Focus inside the preview pauses motion so controls and
content stay usable.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module">
const ticker = window.Ticker.ticker
const news = ticker('#ticker-1', { duration: 24, direction: 'left', pauseOnHover: true })
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
const duration = example.querySelector('input')
const output = example.querySelector('output')
duration.addEventListener(
'input',
() => {
news.update({ duration: Number(duration.value) })
output.value = duration.value + 's'
},
{ signal }
)
const direction = example.querySelector('[data-example-direction-select]')
direction.addEventListener(
'change',
() => {
news.update({ direction: direction.value })
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
</script>
</body>
</html>
Package / ESM
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Speed and direction</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="Speed and direction">
<section class="ticker-recipe">
<h1>Speed and direction</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Adjustable ticker"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item">Your content</span>
<span class="item">Your direction</span>
<span class="item">Your pace</span>
<span class="item">One small library</span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
<label
>Loop duration (seconds)
<input
type="range"
min="14"
max="60"
value="24"
aria-label="Loop duration (seconds)"
/><output>24s</output></label
>
<label
>Direction
<select data-example-direction-select aria-label="Direction">
<option value="left">Left</option>
<option value="right">Right</option>
</select></label
>
</div>
<p>
A shorter duration moves faster. Focus inside the preview pauses motion so controls and
content stay usable.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const news = ticker('#ticker-1', { duration: 24, direction: 'left', pauseOnHover: true })
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
const duration = example.querySelector('input')
const output = example.querySelector('output')
duration.addEventListener(
'input',
() => {
news.update({ duration: Number(duration.value) })
output.value = duration.value + 's'
},
{ signal }
)
const direction = example.querySelector('[data-example-direction-select]')
direction.addEventListener(
'change',
() => {
news.update({ direction: direction.value })
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Speed and direction</title>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="Speed and direction">
<section class="ticker-recipe">
<h1>Speed and direction</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Adjustable ticker"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item">Your content</span>
<span class="item">Your direction</span>
<span class="item">Your pace</span>
<span class="item">One small library</span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
<label
>Loop duration (seconds)
<input
type="range"
min="14"
max="60"
value="24"
aria-label="Loop duration (seconds)"
/><output>24s</output></label
>
<label
>Direction
<select data-example-direction-select aria-label="Direction">
<option value="left">Left</option>
<option value="right">Right</option>
</select></label
>
</div>
<p>
A shorter duration moves faster. Focus inside the preview pauses motion so controls and
content stay usable.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module">
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const news = ticker('#ticker-1', { duration: 24, direction: 'left', pauseOnHover: true })
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
const duration = example.querySelector('input')
const output = example.querySelector('output')
duration.addEventListener(
'input',
() => {
news.update({ duration: Number(duration.value) })
output.value = duration.value + 's'
},
{ signal }
)
const direction = example.querySelector('[data-example-direction-select]')
direction.addEventListener(
'change',
() => {
news.update({ direction: direction.value })
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
</script>
</body>
</html>
Vertical news
A scrolling resource feed with illustrated cards, useful links and descriptions. Each card keeps its own height while the column moves at 28 pixels per second.
Initialize a ticker after the page mounts and keep its original links available.
Use pixel speed and viewport rules to keep a feed readable as its layout changes.
Add items to the original source, refresh its copies and release the controller on cleanup.
Give keyboard users time to read with focus pause and respect reduced motion preferences.
Illustrative technology marks, not product announcements or endorsements. Choose Up or Down to change movement. Hover or focus a link to read; use Previous, Next, the rail or drag to browse.
Open code · HTML / CDN or Package / ESM
HTML / CDN opens directly as an index.html file with an internet connection; the CDN script exposes window.Ticker. For Package / ESM, install @samline/ticker and use the file in an HTML-entry bundler such as Vite. Both use the same ticker() API; a registry is optional.
Read or copy HTML, CSS and JavaScript as three files in the same folder. Use Complete file to copy one runnable index.html instead. Copied code uses the initial settings; preview changes are not included.
HTML / CDN
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Vertical news</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="Vertical news">
<section class="ticker-recipe">
<h1>Vertical news</h1>
<div
class="window"
dir="ltr"
data-vertical="true"
tabindex="0"
role="region"
aria-label="Vertical ticker resources"
aria-live="off"
>
<div id="ticker-1" data-row>
<div class="item story-item">
<span class="story-image"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EAstro%3C%2Ftitle%3E%3Cpath%20d%3D%22M8.358%2020.162c-1.186-1.07-1.532-3.316-1.038-4.944.856%201.026%202.043%201.352%203.272%201.535%201.897.283%203.76.177%205.522-.678.202-.098.388-.229.608-.36.166.473.209.95.151%201.437-.14%201.185-.738%202.1-1.688%202.794-.38.277-.782.525-1.175.787-1.205.804-1.531%201.747-1.078%203.119l.044.148a3.158%203.158%200%200%201-1.407-1.188%203.31%203.31%200%200%201-.544-1.815c-.004-.32-.004-.642-.048-.958-.106-.769-.472-1.113-1.161-1.133-.707-.02-1.267.411-1.415%201.09-.012.053-.028.104-.045.165h.002zm-5.961-4.445s3.24-1.575%206.49-1.575l2.451-7.565c.092-.366.36-.614.662-.614.302%200%20.57.248.662.614l2.45%207.565c3.85%200%206.491%201.575%206.491%201.575L16.088.727C15.93.285%2015.663%200%2015.303%200H8.697c-.36%200-.615.285-.784.727l-5.516%2014.99z%22%2F%3E%3C%2Fsvg%3E%0A"
alt="" /></span
><span class="story-copy"
><a href="https://samline.github.io/ticker/docs/getting-started/"
>Astro: start with your own markup</a
><span
>Initialize a ticker after the page mounts and keep its original links
available.</span
></span
>
</div>
<div class="item story-item">
<span class="story-image"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EVite%3C%2Ftitle%3E%3Cpath%20d%3D%22m8.286%2010.578.512-8.657a.306.306%200%200%201%20.247-.282L17.377.006a.306.306%200%200%201%20.353.385l-1.558%205.403a.306.306%200%200%200%20.352.385l2.388-.46a.306.306%200%200%201%20.332.438l-6.79%2013.55-.123.19a.294.294%200%200%201-.252.14c-.177%200-.35-.152-.305-.369l1.095-5.301a.306.306%200%200%200-.388-.355l-1.433.435a.306.306%200%200%201-.389-.354l.69-3.375a.306.306%200%200%200-.37-.36l-2.32.536a.306.306%200%200%201-.374-.316zm14.976-7.926L17.284%203.74l-.544%201.887%202.077-.4a.8.8%200%200%201%20.84.369.8.8%200%200%201%20.034.783L12.9%2019.93l-.013.025-.015.023-.122.19a.801.801%200%200%201-.672.37.826.826%200%200%201-.634-.302.8.8%200%200%201-.16-.67l1.029-4.981-1.12.34a.81.81%200%200%201-.86-.262.802.802%200%200%201-.165-.67l.63-3.08-2.027.468a.808.808%200%200%201-.768-.233.81.81%200%200%201-.217-.6l.389-6.57-7.44-1.33a.612.612%200%200%200-.64.906L11.58%2023.691a.612.612%200%200%200%201.066-.004l11.26-20.135a.612.612%200%200%200-.644-.9z%22%2F%3E%3C%2Fsvg%3E%0A"
alt="" /></span
><span class="story-copy"
><a href="https://samline.github.io/ticker/docs/reference/configuration/"
>Vite: choose a steady reading pace</a
><span
>Use pixel speed and viewport rules to keep a feed readable as its layout
changes.</span
></span
>
</div>
<div class="item story-item">
<span class="story-image"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EGitHub%3C%2Ftitle%3E%3Cpath%20d%3D%22M12%20.297c-6.63%200-12%205.373-12%2012%200%205.303%203.438%209.8%208.205%2011.385.6.113.82-.258.82-.577%200-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422%2018.07%203.633%2017.7%203.633%2017.7c-1.087-.744.084-.729.084-.729%201.205.084%201.838%201.236%201.838%201.236%201.07%201.835%202.809%201.305%203.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93%200-1.31.465-2.38%201.235-3.22-.135-.303-.54-1.523.105-3.176%200%200%201.005-.322%203.3%201.23.96-.267%201.98-.399%203-.405%201.02.006%202.04.138%203%20.405%202.28-1.552%203.285-1.23%203.285-1.23.645%201.653.24%202.873.12%203.176.765.84%201.23%201.91%201.23%203.22%200%204.61-2.805%205.625-5.475%205.92.42.36.81%201.096.81%202.22%200%201.606-.015%202.896-.015%203.286%200%20.315.21.69.825.57C20.565%2022.092%2024%2017.592%2024%2012.297c0-6.627-5.373-12-12-12%22%2F%3E%3C%2Fsvg%3E%0A"
alt="" /></span
><span class="story-copy"
><a href="https://samline.github.io/ticker/docs/guides/lifecycle/"
>GitHub: keep live content in sync</a
><span
>Add items to the original source, refresh its copies and release the controller
on cleanup.</span
></span
>
</div>
<div class="item story-item">
<span class="story-image"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EFigma%3C%2Ftitle%3E%3Cpath%20d%3D%22M15.852%208.981h-4.588V0h4.588c2.476%200%204.49%202.014%204.49%204.49s-2.014%204.491-4.49%204.491zM12.735%207.51h3.117c1.665%200%203.019-1.355%203.019-3.019s-1.355-3.019-3.019-3.019h-3.117V7.51zm0%201.471H8.148c-2.476%200-4.49-2.014-4.49-4.49S5.672%200%208.148%200h4.588v8.981zm-4.587-7.51c-1.665%200-3.019%201.355-3.019%203.019s1.354%203.02%203.019%203.02h3.117V1.471H8.148zm4.587%2015.019H8.148c-2.476%200-4.49-2.014-4.49-4.49s2.014-4.49%204.49-4.49h4.588v8.98zM8.148%208.981c-1.665%200-3.019%201.355-3.019%203.019s1.355%203.019%203.019%203.019h3.117V8.981H8.148zM8.172%2024c-2.489%200-4.515-2.014-4.515-4.49s2.014-4.49%204.49-4.49h4.588v4.441c0%202.503-2.047%204.539-4.563%204.539zm-.024-7.51a3.023%203.023%200%200%200-3.019%203.019c0%201.665%201.365%203.019%203.044%203.019%201.705%200%203.093-1.376%203.093-3.068v-2.97H8.148zm7.704%200h-.098c-2.476%200-4.49-2.014-4.49-4.49s2.014-4.49%204.49-4.49h.098c2.476%200%204.49%202.014%204.49%204.49s-2.014%204.49-4.49%204.49zm-.097-7.509c-1.665%200-3.019%201.355-3.019%203.019s1.355%203.019%203.019%203.019h.098c1.665%200%203.019-1.355%203.019-3.019s-1.355-3.019-3.019-3.019h-.098z%22%2F%3E%3C%2Fsvg%3E%0A"
alt="" /></span
><span class="story-copy"
><a href="https://samline.github.io/ticker/docs/guides/accessibility/"
>Figma: make motion accessible</a
><span
>Give keyboard users time to read with focus pause and respect reduced motion
preferences.</span
></span
>
</div>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
<button type="button" data-previous>Previous items</button
><button type="button" data-next>Next items</button>
<label
>Position in repeated sequence
<input type="range" data-position min="0" max="1" value="0"
/></label>
<div role="group" aria-label="Direction">
<button type="button" data-example-direction="up" aria-pressed="true">Up</button
><button type="button" data-example-direction="down" aria-pressed="false">Down</button>
</div>
</div>
<p>
Illustrative technology marks, not product announcements or endorsements. Choose Up or
Down to change movement. Hover or focus a link to read; use Previous, Next, the rail or
drag to browse.
</p>
<p>
<a
href="https://samline.github.io/ticker/attribution/"
target="_blank"
rel="noopener noreferrer"
>Demo brand marks and attribution</a
>
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
const ticker = window.Ticker.ticker
const news = ticker('#ticker-1', {
duration: 20,
speed: 28,
direction: 'up',
navigation: true,
pauseOnHover: true,
})
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
news.connectControls({
previous: example.querySelector('[data-previous]'),
next: example.querySelector('[data-next]'),
rail: example.querySelector('[data-position]'),
})
const directions = example.querySelectorAll('button[data-example-direction]')
directions.forEach(button => {
button.addEventListener(
'click',
() => {
news.update({ direction: button.dataset.exampleDirection })
directions.forEach(item => item.setAttribute('aria-pressed', String(item === button)))
},
{ signal }
)
})
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Vertical news</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="Vertical news">
<section class="ticker-recipe">
<h1>Vertical news</h1>
<div
class="window"
dir="ltr"
data-vertical="true"
tabindex="0"
role="region"
aria-label="Vertical ticker resources"
aria-live="off"
>
<div id="ticker-1" data-row>
<div class="item story-item">
<span class="story-image"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EAstro%3C%2Ftitle%3E%3Cpath%20d%3D%22M8.358%2020.162c-1.186-1.07-1.532-3.316-1.038-4.944.856%201.026%202.043%201.352%203.272%201.535%201.897.283%203.76.177%205.522-.678.202-.098.388-.229.608-.36.166.473.209.95.151%201.437-.14%201.185-.738%202.1-1.688%202.794-.38.277-.782.525-1.175.787-1.205.804-1.531%201.747-1.078%203.119l.044.148a3.158%203.158%200%200%201-1.407-1.188%203.31%203.31%200%200%201-.544-1.815c-.004-.32-.004-.642-.048-.958-.106-.769-.472-1.113-1.161-1.133-.707-.02-1.267.411-1.415%201.09-.012.053-.028.104-.045.165h.002zm-5.961-4.445s3.24-1.575%206.49-1.575l2.451-7.565c.092-.366.36-.614.662-.614.302%200%20.57.248.662.614l2.45%207.565c3.85%200%206.491%201.575%206.491%201.575L16.088.727C15.93.285%2015.663%200%2015.303%200H8.697c-.36%200-.615.285-.784.727l-5.516%2014.99z%22%2F%3E%3C%2Fsvg%3E%0A"
alt="" /></span
><span class="story-copy"
><a href="https://samline.github.io/ticker/docs/getting-started/"
>Astro: start with your own markup</a
><span
>Initialize a ticker after the page mounts and keep its original links
available.</span
></span
>
</div>
<div class="item story-item">
<span class="story-image"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EVite%3C%2Ftitle%3E%3Cpath%20d%3D%22m8.286%2010.578.512-8.657a.306.306%200%200%201%20.247-.282L17.377.006a.306.306%200%200%201%20.353.385l-1.558%205.403a.306.306%200%200%200%20.352.385l2.388-.46a.306.306%200%200%201%20.332.438l-6.79%2013.55-.123.19a.294.294%200%200%201-.252.14c-.177%200-.35-.152-.305-.369l1.095-5.301a.306.306%200%200%200-.388-.355l-1.433.435a.306.306%200%200%201-.389-.354l.69-3.375a.306.306%200%200%200-.37-.36l-2.32.536a.306.306%200%200%201-.374-.316zm14.976-7.926L17.284%203.74l-.544%201.887%202.077-.4a.8.8%200%200%201%20.84.369.8.8%200%200%201%20.034.783L12.9%2019.93l-.013.025-.015.023-.122.19a.801.801%200%200%201-.672.37.826.826%200%200%201-.634-.302.8.8%200%200%201-.16-.67l1.029-4.981-1.12.34a.81.81%200%200%201-.86-.262.802.802%200%200%201-.165-.67l.63-3.08-2.027.468a.808.808%200%200%201-.768-.233.81.81%200%200%201-.217-.6l.389-6.57-7.44-1.33a.612.612%200%200%200-.64.906L11.58%2023.691a.612.612%200%200%200%201.066-.004l11.26-20.135a.612.612%200%200%200-.644-.9z%22%2F%3E%3C%2Fsvg%3E%0A"
alt="" /></span
><span class="story-copy"
><a href="https://samline.github.io/ticker/docs/reference/configuration/"
>Vite: choose a steady reading pace</a
><span
>Use pixel speed and viewport rules to keep a feed readable as its layout
changes.</span
></span
>
</div>
<div class="item story-item">
<span class="story-image"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EGitHub%3C%2Ftitle%3E%3Cpath%20d%3D%22M12%20.297c-6.63%200-12%205.373-12%2012%200%205.303%203.438%209.8%208.205%2011.385.6.113.82-.258.82-.577%200-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422%2018.07%203.633%2017.7%203.633%2017.7c-1.087-.744.084-.729.084-.729%201.205.084%201.838%201.236%201.838%201.236%201.07%201.835%202.809%201.305%203.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93%200-1.31.465-2.38%201.235-3.22-.135-.303-.54-1.523.105-3.176%200%200%201.005-.322%203.3%201.23.96-.267%201.98-.399%203-.405%201.02.006%202.04.138%203%20.405%202.28-1.552%203.285-1.23%203.285-1.23.645%201.653.24%202.873.12%203.176.765.84%201.23%201.91%201.23%203.22%200%204.61-2.805%205.625-5.475%205.92.42.36.81%201.096.81%202.22%200%201.606-.015%202.896-.015%203.286%200%20.315.21.69.825.57C20.565%2022.092%2024%2017.592%2024%2012.297c0-6.627-5.373-12-12-12%22%2F%3E%3C%2Fsvg%3E%0A"
alt="" /></span
><span class="story-copy"
><a href="https://samline.github.io/ticker/docs/guides/lifecycle/"
>GitHub: keep live content in sync</a
><span
>Add items to the original source, refresh its copies and release the controller
on cleanup.</span
></span
>
</div>
<div class="item story-item">
<span class="story-image"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EFigma%3C%2Ftitle%3E%3Cpath%20d%3D%22M15.852%208.981h-4.588V0h4.588c2.476%200%204.49%202.014%204.49%204.49s-2.014%204.491-4.49%204.491zM12.735%207.51h3.117c1.665%200%203.019-1.355%203.019-3.019s-1.355-3.019-3.019-3.019h-3.117V7.51zm0%201.471H8.148c-2.476%200-4.49-2.014-4.49-4.49S5.672%200%208.148%200h4.588v8.981zm-4.587-7.51c-1.665%200-3.019%201.355-3.019%203.019s1.354%203.02%203.019%203.02h3.117V1.471H8.148zm4.587%2015.019H8.148c-2.476%200-4.49-2.014-4.49-4.49s2.014-4.49%204.49-4.49h4.588v8.98zM8.148%208.981c-1.665%200-3.019%201.355-3.019%203.019s1.355%203.019%203.019%203.019h3.117V8.981H8.148zM8.172%2024c-2.489%200-4.515-2.014-4.515-4.49s2.014-4.49%204.49-4.49h4.588v4.441c0%202.503-2.047%204.539-4.563%204.539zm-.024-7.51a3.023%203.023%200%200%200-3.019%203.019c0%201.665%201.365%203.019%203.044%203.019%201.705%200%203.093-1.376%203.093-3.068v-2.97H8.148zm7.704%200h-.098c-2.476%200-4.49-2.014-4.49-4.49s2.014-4.49%204.49-4.49h.098c2.476%200%204.49%202.014%204.49%204.49s-2.014%204.49-4.49%204.49zm-.097-7.509c-1.665%200-3.019%201.355-3.019%203.019s1.355%203.019%203.019%203.019h.098c1.665%200%203.019-1.355%203.019-3.019s-1.355-3.019-3.019-3.019h-.098z%22%2F%3E%3C%2Fsvg%3E%0A"
alt="" /></span
><span class="story-copy"
><a href="https://samline.github.io/ticker/docs/guides/accessibility/"
>Figma: make motion accessible</a
><span
>Give keyboard users time to read with focus pause and respect reduced motion
preferences.</span
></span
>
</div>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
<button type="button" data-previous>Previous items</button
><button type="button" data-next>Next items</button>
<label
>Position in repeated sequence
<input type="range" data-position min="0" max="1" value="0"
/></label>
<div role="group" aria-label="Direction">
<button type="button" data-example-direction="up" aria-pressed="true">Up</button
><button type="button" data-example-direction="down" aria-pressed="false">Down</button>
</div>
</div>
<p>
Illustrative technology marks, not product announcements or endorsements. Choose Up or
Down to change movement. Hover or focus a link to read; use Previous, Next, the rail or
drag to browse.
</p>
<p>
<a
href="https://samline.github.io/ticker/attribution/"
target="_blank"
rel="noopener noreferrer"
>Demo brand marks and attribution</a
>
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module">
const ticker = window.Ticker.ticker
const news = ticker('#ticker-1', {
duration: 20,
speed: 28,
direction: 'up',
navigation: true,
pauseOnHover: true,
})
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
news.connectControls({
previous: example.querySelector('[data-previous]'),
next: example.querySelector('[data-next]'),
rail: example.querySelector('[data-position]'),
})
const directions = example.querySelectorAll('button[data-example-direction]')
directions.forEach(button => {
button.addEventListener(
'click',
() => {
news.update({ direction: button.dataset.exampleDirection })
directions.forEach(item => item.setAttribute('aria-pressed', String(item === button)))
},
{ signal }
)
})
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
</script>
</body>
</html>
Package / ESM
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Vertical news</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="Vertical news">
<section class="ticker-recipe">
<h1>Vertical news</h1>
<div
class="window"
dir="ltr"
data-vertical="true"
tabindex="0"
role="region"
aria-label="Vertical ticker resources"
aria-live="off"
>
<div id="ticker-1" data-row>
<div class="item story-item">
<span class="story-image"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EAstro%3C%2Ftitle%3E%3Cpath%20d%3D%22M8.358%2020.162c-1.186-1.07-1.532-3.316-1.038-4.944.856%201.026%202.043%201.352%203.272%201.535%201.897.283%203.76.177%205.522-.678.202-.098.388-.229.608-.36.166.473.209.95.151%201.437-.14%201.185-.738%202.1-1.688%202.794-.38.277-.782.525-1.175.787-1.205.804-1.531%201.747-1.078%203.119l.044.148a3.158%203.158%200%200%201-1.407-1.188%203.31%203.31%200%200%201-.544-1.815c-.004-.32-.004-.642-.048-.958-.106-.769-.472-1.113-1.161-1.133-.707-.02-1.267.411-1.415%201.09-.012.053-.028.104-.045.165h.002zm-5.961-4.445s3.24-1.575%206.49-1.575l2.451-7.565c.092-.366.36-.614.662-.614.302%200%20.57.248.662.614l2.45%207.565c3.85%200%206.491%201.575%206.491%201.575L16.088.727C15.93.285%2015.663%200%2015.303%200H8.697c-.36%200-.615.285-.784.727l-5.516%2014.99z%22%2F%3E%3C%2Fsvg%3E%0A"
alt="" /></span
><span class="story-copy"
><a href="https://samline.github.io/ticker/docs/getting-started/"
>Astro: start with your own markup</a
><span
>Initialize a ticker after the page mounts and keep its original links
available.</span
></span
>
</div>
<div class="item story-item">
<span class="story-image"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EVite%3C%2Ftitle%3E%3Cpath%20d%3D%22m8.286%2010.578.512-8.657a.306.306%200%200%201%20.247-.282L17.377.006a.306.306%200%200%201%20.353.385l-1.558%205.403a.306.306%200%200%200%20.352.385l2.388-.46a.306.306%200%200%201%20.332.438l-6.79%2013.55-.123.19a.294.294%200%200%201-.252.14c-.177%200-.35-.152-.305-.369l1.095-5.301a.306.306%200%200%200-.388-.355l-1.433.435a.306.306%200%200%201-.389-.354l.69-3.375a.306.306%200%200%200-.37-.36l-2.32.536a.306.306%200%200%201-.374-.316zm14.976-7.926L17.284%203.74l-.544%201.887%202.077-.4a.8.8%200%200%201%20.84.369.8.8%200%200%201%20.034.783L12.9%2019.93l-.013.025-.015.023-.122.19a.801.801%200%200%201-.672.37.826.826%200%200%201-.634-.302.8.8%200%200%201-.16-.67l1.029-4.981-1.12.34a.81.81%200%200%201-.86-.262.802.802%200%200%201-.165-.67l.63-3.08-2.027.468a.808.808%200%200%201-.768-.233.81.81%200%200%201-.217-.6l.389-6.57-7.44-1.33a.612.612%200%200%200-.64.906L11.58%2023.691a.612.612%200%200%200%201.066-.004l11.26-20.135a.612.612%200%200%200-.644-.9z%22%2F%3E%3C%2Fsvg%3E%0A"
alt="" /></span
><span class="story-copy"
><a href="https://samline.github.io/ticker/docs/reference/configuration/"
>Vite: choose a steady reading pace</a
><span
>Use pixel speed and viewport rules to keep a feed readable as its layout
changes.</span
></span
>
</div>
<div class="item story-item">
<span class="story-image"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EGitHub%3C%2Ftitle%3E%3Cpath%20d%3D%22M12%20.297c-6.63%200-12%205.373-12%2012%200%205.303%203.438%209.8%208.205%2011.385.6.113.82-.258.82-.577%200-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422%2018.07%203.633%2017.7%203.633%2017.7c-1.087-.744.084-.729.084-.729%201.205.084%201.838%201.236%201.838%201.236%201.07%201.835%202.809%201.305%203.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93%200-1.31.465-2.38%201.235-3.22-.135-.303-.54-1.523.105-3.176%200%200%201.005-.322%203.3%201.23.96-.267%201.98-.399%203-.405%201.02.006%202.04.138%203%20.405%202.28-1.552%203.285-1.23%203.285-1.23.645%201.653.24%202.873.12%203.176.765.84%201.23%201.91%201.23%203.22%200%204.61-2.805%205.625-5.475%205.92.42.36.81%201.096.81%202.22%200%201.606-.015%202.896-.015%203.286%200%20.315.21.69.825.57C20.565%2022.092%2024%2017.592%2024%2012.297c0-6.627-5.373-12-12-12%22%2F%3E%3C%2Fsvg%3E%0A"
alt="" /></span
><span class="story-copy"
><a href="https://samline.github.io/ticker/docs/guides/lifecycle/"
>GitHub: keep live content in sync</a
><span
>Add items to the original source, refresh its copies and release the controller
on cleanup.</span
></span
>
</div>
<div class="item story-item">
<span class="story-image"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EFigma%3C%2Ftitle%3E%3Cpath%20d%3D%22M15.852%208.981h-4.588V0h4.588c2.476%200%204.49%202.014%204.49%204.49s-2.014%204.491-4.49%204.491zM12.735%207.51h3.117c1.665%200%203.019-1.355%203.019-3.019s-1.355-3.019-3.019-3.019h-3.117V7.51zm0%201.471H8.148c-2.476%200-4.49-2.014-4.49-4.49S5.672%200%208.148%200h4.588v8.981zm-4.587-7.51c-1.665%200-3.019%201.355-3.019%203.019s1.354%203.02%203.019%203.02h3.117V1.471H8.148zm4.587%2015.019H8.148c-2.476%200-4.49-2.014-4.49-4.49s2.014-4.49%204.49-4.49h4.588v8.98zM8.148%208.981c-1.665%200-3.019%201.355-3.019%203.019s1.355%203.019%203.019%203.019h3.117V8.981H8.148zM8.172%2024c-2.489%200-4.515-2.014-4.515-4.49s2.014-4.49%204.49-4.49h4.588v4.441c0%202.503-2.047%204.539-4.563%204.539zm-.024-7.51a3.023%203.023%200%200%200-3.019%203.019c0%201.665%201.365%203.019%203.044%203.019%201.705%200%203.093-1.376%203.093-3.068v-2.97H8.148zm7.704%200h-.098c-2.476%200-4.49-2.014-4.49-4.49s2.014-4.49%204.49-4.49h.098c2.476%200%204.49%202.014%204.49%204.49s-2.014%204.49-4.49%204.49zm-.097-7.509c-1.665%200-3.019%201.355-3.019%203.019s1.355%203.019%203.019%203.019h.098c1.665%200%203.019-1.355%203.019-3.019s-1.355-3.019-3.019-3.019h-.098z%22%2F%3E%3C%2Fsvg%3E%0A"
alt="" /></span
><span class="story-copy"
><a href="https://samline.github.io/ticker/docs/guides/accessibility/"
>Figma: make motion accessible</a
><span
>Give keyboard users time to read with focus pause and respect reduced motion
preferences.</span
></span
>
</div>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
<button type="button" data-previous>Previous items</button
><button type="button" data-next>Next items</button>
<label
>Position in repeated sequence
<input type="range" data-position min="0" max="1" value="0"
/></label>
<div role="group" aria-label="Direction">
<button type="button" data-example-direction="up" aria-pressed="true">Up</button
><button type="button" data-example-direction="down" aria-pressed="false">Down</button>
</div>
</div>
<p>
Illustrative technology marks, not product announcements or endorsements. Choose Up or
Down to change movement. Hover or focus a link to read; use Previous, Next, the rail or
drag to browse.
</p>
<p>
<a
href="https://samline.github.io/ticker/attribution/"
target="_blank"
rel="noopener noreferrer"
>Demo brand marks and attribution</a
>
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const news = ticker('#ticker-1', {
duration: 20,
speed: 28,
direction: 'up',
navigation: true,
pauseOnHover: true,
})
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
news.connectControls({
previous: example.querySelector('[data-previous]'),
next: example.querySelector('[data-next]'),
rail: example.querySelector('[data-position]'),
})
const directions = example.querySelectorAll('button[data-example-direction]')
directions.forEach(button => {
button.addEventListener(
'click',
() => {
news.update({ direction: button.dataset.exampleDirection })
directions.forEach(item => item.setAttribute('aria-pressed', String(item === button)))
},
{ signal }
)
})
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Vertical news</title>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="Vertical news">
<section class="ticker-recipe">
<h1>Vertical news</h1>
<div
class="window"
dir="ltr"
data-vertical="true"
tabindex="0"
role="region"
aria-label="Vertical ticker resources"
aria-live="off"
>
<div id="ticker-1" data-row>
<div class="item story-item">
<span class="story-image"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EAstro%3C%2Ftitle%3E%3Cpath%20d%3D%22M8.358%2020.162c-1.186-1.07-1.532-3.316-1.038-4.944.856%201.026%202.043%201.352%203.272%201.535%201.897.283%203.76.177%205.522-.678.202-.098.388-.229.608-.36.166.473.209.95.151%201.437-.14%201.185-.738%202.1-1.688%202.794-.38.277-.782.525-1.175.787-1.205.804-1.531%201.747-1.078%203.119l.044.148a3.158%203.158%200%200%201-1.407-1.188%203.31%203.31%200%200%201-.544-1.815c-.004-.32-.004-.642-.048-.958-.106-.769-.472-1.113-1.161-1.133-.707-.02-1.267.411-1.415%201.09-.012.053-.028.104-.045.165h.002zm-5.961-4.445s3.24-1.575%206.49-1.575l2.451-7.565c.092-.366.36-.614.662-.614.302%200%20.57.248.662.614l2.45%207.565c3.85%200%206.491%201.575%206.491%201.575L16.088.727C15.93.285%2015.663%200%2015.303%200H8.697c-.36%200-.615.285-.784.727l-5.516%2014.99z%22%2F%3E%3C%2Fsvg%3E%0A"
alt="" /></span
><span class="story-copy"
><a href="https://samline.github.io/ticker/docs/getting-started/"
>Astro: start with your own markup</a
><span
>Initialize a ticker after the page mounts and keep its original links
available.</span
></span
>
</div>
<div class="item story-item">
<span class="story-image"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EVite%3C%2Ftitle%3E%3Cpath%20d%3D%22m8.286%2010.578.512-8.657a.306.306%200%200%201%20.247-.282L17.377.006a.306.306%200%200%201%20.353.385l-1.558%205.403a.306.306%200%200%200%20.352.385l2.388-.46a.306.306%200%200%201%20.332.438l-6.79%2013.55-.123.19a.294.294%200%200%201-.252.14c-.177%200-.35-.152-.305-.369l1.095-5.301a.306.306%200%200%200-.388-.355l-1.433.435a.306.306%200%200%201-.389-.354l.69-3.375a.306.306%200%200%200-.37-.36l-2.32.536a.306.306%200%200%201-.374-.316zm14.976-7.926L17.284%203.74l-.544%201.887%202.077-.4a.8.8%200%200%201%20.84.369.8.8%200%200%201%20.034.783L12.9%2019.93l-.013.025-.015.023-.122.19a.801.801%200%200%201-.672.37.826.826%200%200%201-.634-.302.8.8%200%200%201-.16-.67l1.029-4.981-1.12.34a.81.81%200%200%201-.86-.262.802.802%200%200%201-.165-.67l.63-3.08-2.027.468a.808.808%200%200%201-.768-.233.81.81%200%200%201-.217-.6l.389-6.57-7.44-1.33a.612.612%200%200%200-.64.906L11.58%2023.691a.612.612%200%200%200%201.066-.004l11.26-20.135a.612.612%200%200%200-.644-.9z%22%2F%3E%3C%2Fsvg%3E%0A"
alt="" /></span
><span class="story-copy"
><a href="https://samline.github.io/ticker/docs/reference/configuration/"
>Vite: choose a steady reading pace</a
><span
>Use pixel speed and viewport rules to keep a feed readable as its layout
changes.</span
></span
>
</div>
<div class="item story-item">
<span class="story-image"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EGitHub%3C%2Ftitle%3E%3Cpath%20d%3D%22M12%20.297c-6.63%200-12%205.373-12%2012%200%205.303%203.438%209.8%208.205%2011.385.6.113.82-.258.82-.577%200-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422%2018.07%203.633%2017.7%203.633%2017.7c-1.087-.744.084-.729.084-.729%201.205.084%201.838%201.236%201.838%201.236%201.07%201.835%202.809%201.305%203.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93%200-1.31.465-2.38%201.235-3.22-.135-.303-.54-1.523.105-3.176%200%200%201.005-.322%203.3%201.23.96-.267%201.98-.399%203-.405%201.02.006%202.04.138%203%20.405%202.28-1.552%203.285-1.23%203.285-1.23.645%201.653.24%202.873.12%203.176.765.84%201.23%201.91%201.23%203.22%200%204.61-2.805%205.625-5.475%205.92.42.36.81%201.096.81%202.22%200%201.606-.015%202.896-.015%203.286%200%20.315.21.69.825.57C20.565%2022.092%2024%2017.592%2024%2012.297c0-6.627-5.373-12-12-12%22%2F%3E%3C%2Fsvg%3E%0A"
alt="" /></span
><span class="story-copy"
><a href="https://samline.github.io/ticker/docs/guides/lifecycle/"
>GitHub: keep live content in sync</a
><span
>Add items to the original source, refresh its copies and release the controller
on cleanup.</span
></span
>
</div>
<div class="item story-item">
<span class="story-image"
><img
src="data:image/svg+xml,%3Csvg%20fill%3D%22%23e0dbe8%22%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EFigma%3C%2Ftitle%3E%3Cpath%20d%3D%22M15.852%208.981h-4.588V0h4.588c2.476%200%204.49%202.014%204.49%204.49s-2.014%204.491-4.49%204.491zM12.735%207.51h3.117c1.665%200%203.019-1.355%203.019-3.019s-1.355-3.019-3.019-3.019h-3.117V7.51zm0%201.471H8.148c-2.476%200-4.49-2.014-4.49-4.49S5.672%200%208.148%200h4.588v8.981zm-4.587-7.51c-1.665%200-3.019%201.355-3.019%203.019s1.354%203.02%203.019%203.02h3.117V1.471H8.148zm4.587%2015.019H8.148c-2.476%200-4.49-2.014-4.49-4.49s2.014-4.49%204.49-4.49h4.588v8.98zM8.148%208.981c-1.665%200-3.019%201.355-3.019%203.019s1.355%203.019%203.019%203.019h3.117V8.981H8.148zM8.172%2024c-2.489%200-4.515-2.014-4.515-4.49s2.014-4.49%204.49-4.49h4.588v4.441c0%202.503-2.047%204.539-4.563%204.539zm-.024-7.51a3.023%203.023%200%200%200-3.019%203.019c0%201.665%201.365%203.019%203.044%203.019%201.705%200%203.093-1.376%203.093-3.068v-2.97H8.148zm7.704%200h-.098c-2.476%200-4.49-2.014-4.49-4.49s2.014-4.49%204.49-4.49h.098c2.476%200%204.49%202.014%204.49%204.49s-2.014%204.49-4.49%204.49zm-.097-7.509c-1.665%200-3.019%201.355-3.019%203.019s1.355%203.019%203.019%203.019h.098c1.665%200%203.019-1.355%203.019-3.019s-1.355-3.019-3.019-3.019h-.098z%22%2F%3E%3C%2Fsvg%3E%0A"
alt="" /></span
><span class="story-copy"
><a href="https://samline.github.io/ticker/docs/guides/accessibility/"
>Figma: make motion accessible</a
><span
>Give keyboard users time to read with focus pause and respect reduced motion
preferences.</span
></span
>
</div>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
<button type="button" data-previous>Previous items</button
><button type="button" data-next>Next items</button>
<label
>Position in repeated sequence
<input type="range" data-position min="0" max="1" value="0"
/></label>
<div role="group" aria-label="Direction">
<button type="button" data-example-direction="up" aria-pressed="true">Up</button
><button type="button" data-example-direction="down" aria-pressed="false">Down</button>
</div>
</div>
<p>
Illustrative technology marks, not product announcements or endorsements. Choose Up or
Down to change movement. Hover or focus a link to read; use Previous, Next, the rail or
drag to browse.
</p>
<p>
<a
href="https://samline.github.io/ticker/attribution/"
target="_blank"
rel="noopener noreferrer"
>Demo brand marks and attribution</a
>
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module">
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const news = ticker('#ticker-1', {
duration: 20,
speed: 28,
direction: 'up',
navigation: true,
pauseOnHover: true,
})
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
news.connectControls({
previous: example.querySelector('[data-previous]'),
next: example.querySelector('[data-next]'),
rail: example.querySelector('[data-position]'),
})
const directions = example.querySelectorAll('button[data-example-direction]')
directions.forEach(button => {
button.addEventListener(
'click',
() => {
news.update({ direction: button.dataset.exampleDirection })
directions.forEach(item => item.setAttribute('aria-pressed', String(item === button)))
},
{ signal }
)
})
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
</script>
</body>
</html>
Duration and constant speed
Compare a fixed 20-second cycle with a longer sequence moving at 40 pixels per second. Speed takes precedence over duration and survives content resizing.
The first row uses duration. The second uses speed, so its duration follows the measured sequence length.
Open code · HTML / CDN or Package / ESM
HTML / CDN opens directly as an index.html file with an internet connection; the CDN script exposes window.Ticker. For Package / ESM, install @samline/ticker and use the file in an HTML-entry bundler such as Vite. Both use the same ticker() API; a registry is optional.
Read or copy HTML, CSS and JavaScript as three files in the same folder. Use Complete file to copy one runnable index.html instead. Copied code uses the initial settings; preview changes are not included.
HTML / CDN
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Duration and constant speed</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="Duration and constant speed">
<section class="ticker-recipe">
<h1>Duration and constant speed</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Fixed duration"
aria-live="off"
>
<div id="ticker-1" data-row>
<a
class="item"
href="https://samline.github.io/ticker/docs/getting-started/"
target="_blank"
rel="noopener noreferrer"
>Start with a working ticker</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/guides/accessibility/"
target="_blank"
rel="noopener noreferrer"
>Make motion accessible</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/reference/configuration/"
target="_blank"
rel="noopener noreferrer"
>Choose your configuration</a
>
</div>
</div>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Constant pixel speed"
aria-live="off"
>
<div id="ticker-2" data-row>
<a
class="item"
href="https://samline.github.io/ticker/docs/getting-started/"
target="_blank"
rel="noopener noreferrer"
>Start with a working ticker</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/guides/accessibility/"
target="_blank"
rel="noopener noreferrer"
>Make motion accessible</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/reference/configuration/"
target="_blank"
rel="noopener noreferrer"
>Choose your configuration</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/getting-started/"
target="_blank"
rel="noopener noreferrer"
>Start with a working ticker</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/guides/accessibility/"
target="_blank"
rel="noopener noreferrer"
>Make motion accessible</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/reference/configuration/"
target="_blank"
rel="noopener noreferrer"
>Choose your configuration</a
>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>
The first row uses duration. The second uses speed, so its duration follows the measured
sequence length.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
const ticker = window.Ticker.ticker
const tickers = [
ticker('#ticker-1', { duration: 20, direction: 'left', pauseOnHover: true }),
ticker('#ticker-2', { duration: 20, speed: 40, direction: 'left', pauseOnHover: true }),
].filter(Boolean)
const news = tickers[0]
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = tickers.some(item => item.state.requested)
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
tickers.forEach(news => {
news.on('playback', renderPlayback)
})
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = tickers.some(item => item.state.requested)
tickers.forEach(news => {
playing ? news.pause() : news.play()
})
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
tickers.forEach(news => {
news.refresh()
})
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
tickers.forEach(news => {
news.destroy()
})
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Duration and constant speed</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="Duration and constant speed">
<section class="ticker-recipe">
<h1>Duration and constant speed</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Fixed duration"
aria-live="off"
>
<div id="ticker-1" data-row>
<a
class="item"
href="https://samline.github.io/ticker/docs/getting-started/"
target="_blank"
rel="noopener noreferrer"
>Start with a working ticker</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/guides/accessibility/"
target="_blank"
rel="noopener noreferrer"
>Make motion accessible</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/reference/configuration/"
target="_blank"
rel="noopener noreferrer"
>Choose your configuration</a
>
</div>
</div>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Constant pixel speed"
aria-live="off"
>
<div id="ticker-2" data-row>
<a
class="item"
href="https://samline.github.io/ticker/docs/getting-started/"
target="_blank"
rel="noopener noreferrer"
>Start with a working ticker</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/guides/accessibility/"
target="_blank"
rel="noopener noreferrer"
>Make motion accessible</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/reference/configuration/"
target="_blank"
rel="noopener noreferrer"
>Choose your configuration</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/getting-started/"
target="_blank"
rel="noopener noreferrer"
>Start with a working ticker</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/guides/accessibility/"
target="_blank"
rel="noopener noreferrer"
>Make motion accessible</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/reference/configuration/"
target="_blank"
rel="noopener noreferrer"
>Choose your configuration</a
>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>
The first row uses duration. The second uses speed, so its duration follows the measured
sequence length.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module">
const ticker = window.Ticker.ticker
const tickers = [
ticker('#ticker-1', { duration: 20, direction: 'left', pauseOnHover: true }),
ticker('#ticker-2', { duration: 20, speed: 40, direction: 'left', pauseOnHover: true }),
].filter(Boolean)
const news = tickers[0]
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = tickers.some(item => item.state.requested)
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
tickers.forEach(news => {
news.on('playback', renderPlayback)
})
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = tickers.some(item => item.state.requested)
tickers.forEach(news => {
playing ? news.pause() : news.play()
})
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
tickers.forEach(news => {
news.refresh()
})
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
tickers.forEach(news => {
news.destroy()
})
},
{ signal }
)
}
</script>
</body>
</html>
Package / ESM
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Duration and constant speed</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="Duration and constant speed">
<section class="ticker-recipe">
<h1>Duration and constant speed</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Fixed duration"
aria-live="off"
>
<div id="ticker-1" data-row>
<a
class="item"
href="https://samline.github.io/ticker/docs/getting-started/"
target="_blank"
rel="noopener noreferrer"
>Start with a working ticker</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/guides/accessibility/"
target="_blank"
rel="noopener noreferrer"
>Make motion accessible</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/reference/configuration/"
target="_blank"
rel="noopener noreferrer"
>Choose your configuration</a
>
</div>
</div>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Constant pixel speed"
aria-live="off"
>
<div id="ticker-2" data-row>
<a
class="item"
href="https://samline.github.io/ticker/docs/getting-started/"
target="_blank"
rel="noopener noreferrer"
>Start with a working ticker</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/guides/accessibility/"
target="_blank"
rel="noopener noreferrer"
>Make motion accessible</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/reference/configuration/"
target="_blank"
rel="noopener noreferrer"
>Choose your configuration</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/getting-started/"
target="_blank"
rel="noopener noreferrer"
>Start with a working ticker</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/guides/accessibility/"
target="_blank"
rel="noopener noreferrer"
>Make motion accessible</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/reference/configuration/"
target="_blank"
rel="noopener noreferrer"
>Choose your configuration</a
>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>
The first row uses duration. The second uses speed, so its duration follows the measured
sequence length.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const tickers = [
ticker('#ticker-1', { duration: 20, direction: 'left', pauseOnHover: true }),
ticker('#ticker-2', { duration: 20, speed: 40, direction: 'left', pauseOnHover: true }),
].filter(Boolean)
const news = tickers[0]
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = tickers.some(item => item.state.requested)
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
tickers.forEach(news => {
news.on('playback', renderPlayback)
})
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = tickers.some(item => item.state.requested)
tickers.forEach(news => {
playing ? news.pause() : news.play()
})
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
tickers.forEach(news => {
news.refresh()
})
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
tickers.forEach(news => {
news.destroy()
})
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Duration and constant speed</title>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="Duration and constant speed">
<section class="ticker-recipe">
<h1>Duration and constant speed</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Fixed duration"
aria-live="off"
>
<div id="ticker-1" data-row>
<a
class="item"
href="https://samline.github.io/ticker/docs/getting-started/"
target="_blank"
rel="noopener noreferrer"
>Start with a working ticker</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/guides/accessibility/"
target="_blank"
rel="noopener noreferrer"
>Make motion accessible</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/reference/configuration/"
target="_blank"
rel="noopener noreferrer"
>Choose your configuration</a
>
</div>
</div>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Constant pixel speed"
aria-live="off"
>
<div id="ticker-2" data-row>
<a
class="item"
href="https://samline.github.io/ticker/docs/getting-started/"
target="_blank"
rel="noopener noreferrer"
>Start with a working ticker</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/guides/accessibility/"
target="_blank"
rel="noopener noreferrer"
>Make motion accessible</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/reference/configuration/"
target="_blank"
rel="noopener noreferrer"
>Choose your configuration</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/getting-started/"
target="_blank"
rel="noopener noreferrer"
>Start with a working ticker</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/guides/accessibility/"
target="_blank"
rel="noopener noreferrer"
>Make motion accessible</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/reference/configuration/"
target="_blank"
rel="noopener noreferrer"
>Choose your configuration</a
>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>
The first row uses duration. The second uses speed, so its duration follows the measured
sequence length.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module">
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const tickers = [
ticker('#ticker-1', { duration: 20, direction: 'left', pauseOnHover: true }),
ticker('#ticker-2', { duration: 20, speed: 40, direction: 'left', pauseOnHover: true }),
].filter(Boolean)
const news = tickers[0]
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = tickers.some(item => item.state.requested)
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
tickers.forEach(news => {
news.on('playback', renderPlayback)
})
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = tickers.some(item => item.state.requested)
tickers.forEach(news => {
playing ? news.pause() : news.play()
})
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
tickers.forEach(news => {
news.refresh()
})
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
tickers.forEach(news => {
news.destroy()
})
},
{ signal }
)
}
</script>
</body>
</html>
RTL content and logical movement
Arabic and Hebrew text retain their writing order alongside numbers and Latin text. Automatic direction follows the inherited writing direction.
Playback starts automatically. The preview has an explicit RTL region inside this LTR page. Previous and Next follow reading order; left and right remain physical animation directions.
Open code · HTML / CDN or Package / ESM
HTML / CDN opens directly as an index.html file with an internet connection; the CDN script exposes window.Ticker. For Package / ESM, install @samline/ticker and use the file in an HTML-entry bundler such as Vite. Both use the same ticker() API; a registry is optional.
Read or copy HTML, CSS and JavaScript as three files in the same folder. Use Complete file to copy one runnable index.html instead. Copied code uses the initial settings; preview changes are not included.
HTML / CDN
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>RTL content and logical movement</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="RTL content and logical movement">
<section class="ticker-recipe">
<h1>RTL content and logical movement</h1>
<div
class="window"
dir="rtl"
data-vertical="false"
tabindex="0"
role="region"
aria-label="RTL mixed content"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item">العربية 123 ABC</span>
<span class="item">עברית 456 XYZ</span>
<span class="item">Release 2.1 — 789</span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
<button type="button" data-previous>Previous items</button
><button type="button" data-next>Next items</button>
<label
>Position in repeated sequence
<input type="range" data-position min="0" max="1" value="0"
/></label>
</div>
<p>
Playback starts automatically. The preview has an explicit RTL region inside this LTR
page. Previous and Next follow reading order; left and right remain physical animation
directions.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
const ticker = window.Ticker.ticker
const news = ticker('#ticker-1', {
duration: 20,
direction: 'auto',
navigation: true,
autoplay: true,
pauseOnHover: true,
})
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
news.connectControls({
previous: example.querySelector('[data-previous]'),
next: example.querySelector('[data-next]'),
rail: example.querySelector('[data-position]'),
})
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>RTL content and logical movement</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="RTL content and logical movement">
<section class="ticker-recipe">
<h1>RTL content and logical movement</h1>
<div
class="window"
dir="rtl"
data-vertical="false"
tabindex="0"
role="region"
aria-label="RTL mixed content"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item">العربية 123 ABC</span>
<span class="item">עברית 456 XYZ</span>
<span class="item">Release 2.1 — 789</span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
<button type="button" data-previous>Previous items</button
><button type="button" data-next>Next items</button>
<label
>Position in repeated sequence
<input type="range" data-position min="0" max="1" value="0"
/></label>
</div>
<p>
Playback starts automatically. The preview has an explicit RTL region inside this LTR
page. Previous and Next follow reading order; left and right remain physical animation
directions.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module">
const ticker = window.Ticker.ticker
const news = ticker('#ticker-1', {
duration: 20,
direction: 'auto',
navigation: true,
autoplay: true,
pauseOnHover: true,
})
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
news.connectControls({
previous: example.querySelector('[data-previous]'),
next: example.querySelector('[data-next]'),
rail: example.querySelector('[data-position]'),
})
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
</script>
</body>
</html>
Package / ESM
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>RTL content and logical movement</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="RTL content and logical movement">
<section class="ticker-recipe">
<h1>RTL content and logical movement</h1>
<div
class="window"
dir="rtl"
data-vertical="false"
tabindex="0"
role="region"
aria-label="RTL mixed content"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item">العربية 123 ABC</span>
<span class="item">עברית 456 XYZ</span>
<span class="item">Release 2.1 — 789</span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
<button type="button" data-previous>Previous items</button
><button type="button" data-next>Next items</button>
<label
>Position in repeated sequence
<input type="range" data-position min="0" max="1" value="0"
/></label>
</div>
<p>
Playback starts automatically. The preview has an explicit RTL region inside this LTR
page. Previous and Next follow reading order; left and right remain physical animation
directions.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const news = ticker('#ticker-1', {
duration: 20,
direction: 'auto',
navigation: true,
autoplay: true,
pauseOnHover: true,
})
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
news.connectControls({
previous: example.querySelector('[data-previous]'),
next: example.querySelector('[data-next]'),
rail: example.querySelector('[data-position]'),
})
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>RTL content and logical movement</title>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="RTL content and logical movement">
<section class="ticker-recipe">
<h1>RTL content and logical movement</h1>
<div
class="window"
dir="rtl"
data-vertical="false"
tabindex="0"
role="region"
aria-label="RTL mixed content"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item">العربية 123 ABC</span>
<span class="item">עברית 456 XYZ</span>
<span class="item">Release 2.1 — 789</span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
<button type="button" data-previous>Previous items</button
><button type="button" data-next>Next items</button>
<label
>Position in repeated sequence
<input type="range" data-position min="0" max="1" value="0"
/></label>
</div>
<p>
Playback starts automatically. The preview has an explicit RTL region inside this LTR
page. Previous and Next follow reading order; left and right remain physical animation
directions.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module">
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const news = ticker('#ticker-1', {
duration: 20,
direction: 'auto',
navigation: true,
autoplay: true,
pauseOnHover: true,
})
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
news.connectControls({
previous: example.querySelector('[data-previous]'),
next: example.querySelector('[data-next]'),
rail: example.querySelector('[data-position]'),
})
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
</script>
</body>
</html>
Responsive motion
Resize the viewport: the same content uses 45 pixels per second on desktop and 24 on narrow screens. Focus and explicit pause remain independent.
Below 48rem the viewport rule overrides speed. Leaving the rule restores the current base speed. Reduced motion still prevents autoplay.
Open code · HTML / CDN or Package / ESM
HTML / CDN opens directly as an index.html file with an internet connection; the CDN script exposes window.Ticker. For Package / ESM, install @samline/ticker and use the file in an HTML-entry bundler such as Vite. Both use the same ticker() API; a registry is optional.
Read or copy HTML, CSS and JavaScript as three files in the same folder. Use Complete file to copy one runnable index.html instead. Copied code uses the initial settings; preview changes are not included.
HTML / CDN
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Responsive motion</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="Responsive motion">
<section class="ticker-recipe">
<h1>Responsive motion</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Responsive news"
aria-live="off"
>
<div id="ticker-1" data-row>
<a
class="item"
href="https://samline.github.io/ticker/docs/getting-started/"
target="_blank"
rel="noopener noreferrer"
>Start with a working ticker</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/guides/accessibility/"
target="_blank"
rel="noopener noreferrer"
>Make motion accessible</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/reference/configuration/"
target="_blank"
rel="noopener noreferrer"
>Choose your configuration</a
>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>
Below 48rem the viewport rule overrides speed. Leaving the rule restores the current base
speed. Reduced motion still prevents autoplay.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
const ticker = window.Ticker.ticker
const news = ticker('#ticker-1', {
duration: 20,
direction: 'left',
speed: 45,
responsive: [{ query: '(width \u003c 48rem)', options: { speed: 24 } }],
pauseOnHover: true,
})
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Responsive motion</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="Responsive motion">
<section class="ticker-recipe">
<h1>Responsive motion</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Responsive news"
aria-live="off"
>
<div id="ticker-1" data-row>
<a
class="item"
href="https://samline.github.io/ticker/docs/getting-started/"
target="_blank"
rel="noopener noreferrer"
>Start with a working ticker</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/guides/accessibility/"
target="_blank"
rel="noopener noreferrer"
>Make motion accessible</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/reference/configuration/"
target="_blank"
rel="noopener noreferrer"
>Choose your configuration</a
>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>
Below 48rem the viewport rule overrides speed. Leaving the rule restores the current base
speed. Reduced motion still prevents autoplay.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module">
const ticker = window.Ticker.ticker
const news = ticker('#ticker-1', {
duration: 20,
direction: 'left',
speed: 45,
responsive: [{ query: '(width \u003c 48rem)', options: { speed: 24 } }],
pauseOnHover: true,
})
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
</script>
</body>
</html>
Package / ESM
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Responsive motion</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="Responsive motion">
<section class="ticker-recipe">
<h1>Responsive motion</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Responsive news"
aria-live="off"
>
<div id="ticker-1" data-row>
<a
class="item"
href="https://samline.github.io/ticker/docs/getting-started/"
target="_blank"
rel="noopener noreferrer"
>Start with a working ticker</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/guides/accessibility/"
target="_blank"
rel="noopener noreferrer"
>Make motion accessible</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/reference/configuration/"
target="_blank"
rel="noopener noreferrer"
>Choose your configuration</a
>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>
Below 48rem the viewport rule overrides speed. Leaving the rule restores the current base
speed. Reduced motion still prevents autoplay.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const news = ticker('#ticker-1', {
duration: 20,
direction: 'left',
speed: 45,
responsive: [{ query: '(width \u003c 48rem)', options: { speed: 24 } }],
pauseOnHover: true,
})
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Responsive motion</title>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="Responsive motion">
<section class="ticker-recipe">
<h1>Responsive motion</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Responsive news"
aria-live="off"
>
<div id="ticker-1" data-row>
<a
class="item"
href="https://samline.github.io/ticker/docs/getting-started/"
target="_blank"
rel="noopener noreferrer"
>Start with a working ticker</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/guides/accessibility/"
target="_blank"
rel="noopener noreferrer"
>Make motion accessible</a
>
<a
class="item"
href="https://samline.github.io/ticker/docs/reference/configuration/"
target="_blank"
rel="noopener noreferrer"
>Choose your configuration</a
>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
</div>
<p>
Below 48rem the viewport rule overrides speed. Leaving the rule restores the current base
speed. Reduced motion still prevents autoplay.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module">
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const news = ticker('#ticker-1', {
duration: 20,
direction: 'left',
speed: 45,
responsive: [{ query: '(width \u003c 48rem)', options: { speed: 24 } }],
pauseOnHover: true,
})
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
</script>
</body>
</html>
Drag and choose what happens next
Drag the moving strip with a mouse or touch. Turn Resume after drag on to continue playback after release, or off to leave the strip paused.
With the option on, release and move the pointer away to clear hover pause. With it off, press Play before trying another drag. Changing the option does not start playback or clear a manual pause.
Open code · HTML / CDN or Package / ESM
HTML / CDN opens directly as an index.html file with an internet connection; the CDN script exposes window.Ticker. For Package / ESM, install @samline/ticker and use the file in an HTML-entry bundler such as Vite. Both use the same ticker() API; a registry is optional.
Read or copy HTML, CSS and JavaScript as three files in the same folder. Use Complete file to copy one runnable index.html instead. Copied code uses the initial settings; preview changes are not included.
HTML / CDN
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Drag and choose what happens next</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="Drag and choose what happens next">
<section class="ticker-recipe">
<h1>Drag and choose what happens next</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Drag playback demo"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item">Grab the strip</span>
<span class="item">Drag to explore</span>
<span class="item">Release to compare</span>
<span class="item">Switch resume on or off</span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
<button type="button" data-previous>Previous items</button
><button type="button" data-next>Next items</button>
<label
>Position in repeated sequence
<input type="range" data-position min="0" max="1" value="0" /></label
><button
type="button"
data-example-resume
aria-label="Resume after drag"
aria-pressed="true"
>
Resume after drag <span data-resume-state aria-hidden="true">On</span>
</button>
</div>
<p>
With the option on, release and move the pointer away to clear hover pause. With it off,
press Play before trying another drag. Changing the option does not start playback or
clear a manual pause.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
const ticker = window.Ticker.ticker
const news = ticker('#ticker-1', {
duration: 20,
speed: 40,
direction: 'left',
navigation: true,
resumeAfterDrag: true,
pauseOnHover: true,
})
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
news.connectControls({
previous: example.querySelector('[data-previous]'),
next: example.querySelector('[data-next]'),
rail: example.querySelector('[data-position]'),
})
const resumeButton = example.querySelector('[data-example-resume]')
resumeButton.addEventListener(
'click',
() => {
const enabled = !news.options.resumeAfterDrag
news.update({ resumeAfterDrag: enabled })
resumeButton.setAttribute('aria-pressed', String(enabled))
resumeButton.querySelector('[data-resume-state]').textContent = enabled ? 'On' : 'Off'
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Drag and choose what happens next</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/style.css"
/>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="Drag and choose what happens next">
<section class="ticker-recipe">
<h1>Drag and choose what happens next</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Drag playback demo"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item">Grab the strip</span>
<span class="item">Drag to explore</span>
<span class="item">Release to compare</span>
<span class="item">Switch resume on or off</span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
<button type="button" data-previous>Previous items</button
><button type="button" data-next>Next items</button>
<label
>Position in repeated sequence
<input type="range" data-position min="0" max="1" value="0" /></label
><button
type="button"
data-example-resume
aria-label="Resume after drag"
aria-pressed="true"
>
Resume after drag <span data-resume-state aria-hidden="true">On</span>
</button>
</div>
<p>
With the option on, release and move the pointer away to clear hover pause. With it off,
press Play before trying another drag. Changing the option does not start playback or
clear a manual pause.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@samline/ticker@2.1.0/dist/browser/global.global.js"></script>
<script type="module">
const ticker = window.Ticker.ticker
const news = ticker('#ticker-1', {
duration: 20,
speed: 40,
direction: 'left',
navigation: true,
resumeAfterDrag: true,
pauseOnHover: true,
})
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
news.connectControls({
previous: example.querySelector('[data-previous]'),
next: example.querySelector('[data-next]'),
rail: example.querySelector('[data-position]'),
})
const resumeButton = example.querySelector('[data-example-resume]')
resumeButton.addEventListener(
'click',
() => {
const enabled = !news.options.resumeAfterDrag
news.update({ resumeAfterDrag: enabled })
resumeButton.setAttribute('aria-pressed', String(enabled))
resumeButton.querySelector('[data-resume-state]').textContent = enabled ? 'On' : 'Off'
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
</script>
</body>
</html>
Package / ESM
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Drag and choose what happens next</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main aria-label="Drag and choose what happens next">
<section class="ticker-recipe">
<h1>Drag and choose what happens next</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Drag playback demo"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item">Grab the strip</span>
<span class="item">Drag to explore</span>
<span class="item">Release to compare</span>
<span class="item">Switch resume on or off</span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
<button type="button" data-previous>Previous items</button
><button type="button" data-next>Next items</button>
<label
>Position in repeated sequence
<input type="range" data-position min="0" max="1" value="0" /></label
><button
type="button"
data-example-resume
aria-label="Resume after drag"
aria-pressed="true"
>
Resume after drag <span data-resume-state aria-hidden="true">On</span>
</button>
</div>
<p>
With the option on, release and move the pointer away to clear hover pause. With it off,
press Play before trying another drag. Changing the option does not start playback or
clear a manual pause.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module" src="./main.js"></script>
</body>
</html>
CSS
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
JavaScript
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const news = ticker('#ticker-1', {
duration: 20,
speed: 40,
direction: 'left',
navigation: true,
resumeAfterDrag: true,
pauseOnHover: true,
})
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
news.connectControls({
previous: example.querySelector('[data-previous]'),
next: example.querySelector('[data-next]'),
rail: example.querySelector('[data-position]'),
})
const resumeButton = example.querySelector('[data-example-resume]')
resumeButton.addEventListener(
'click',
() => {
const enabled = !news.options.resumeAfterDrag
news.update({ resumeAfterDrag: enabled })
resumeButton.setAttribute('aria-pressed', String(enabled))
resumeButton.querySelector('[data-resume-state]').textContent = enabled ? 'On' : 'Off'
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
Complete file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Drag and choose what happens next</title>
<style>
body {
margin: 0;
font:
1rem/1.6 system-ui,
sans-serif;
}
.ticker-recipe {
--bg: #faf9fc;
--text: #292233;
--surface: #ffffff;
--border: #c6bdcf;
--accent: #69518d;
--up: #306344;
--down: #a74337;
--hover: #e0d5ed;
display: block;
padding: 1rem;
background: var(--bg);
color: var(--text);
}
@media (prefers-color-scheme: dark) {
.ticker-recipe {
--bg: #1d1c23;
--text: #e5e2eb;
--surface: #292731;
--border: #474250;
--accent: #b7aed2;
--up: #b4c8bb;
--down: #d5aea5;
--hover: #443a4f;
}
}
@scope (.ticker-recipe) {
h1 {
margin: 0 0 1rem;
font-size: 1.125rem;
font-weight: 500;
}
.window {
overflow-x: auto;
margin-block: 0.75rem;
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
[data-row] {
display: flex;
align-items: center;
gap: 1.5rem;
width: max-content;
}
.ticker-track {
gap: 1.5rem;
padding-block: 1rem;
}
.item {
display: inline-flex;
align-items: center;
gap: 0.625rem;
white-space: nowrap;
padding-inline: 0.25rem;
}
a {
color: var(--accent);
text-underline-offset: 0.1875rem;
}
img {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.up {
color: var(--up);
}
.down {
color: var(--down);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
button,
select {
font: inherit;
padding: 0.375rem 0.75rem;
background: var(--surface);
color: var(--text);
border: 0.0625rem solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
}
@media (hover: hover) {
button:hover,
select:hover {
background: var(--hover);
}
}
select {
max-width: 100%;
min-width: 0;
}
label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
input {
accent-color: var(--accent);
width: 6rem;
}
output {
font-variant-numeric: tabular-nums;
}
p {
margin: 0.75rem 0 0;
font-size: 0.8125rem;
}
p:empty {
display: none;
}
[hidden] {
display: none !important;
}
:focus-visible {
outline: 0.125rem solid var(--accent);
outline-offset: 0.1875rem;
}
.window:focus-visible {
outline-offset: -0.1875rem;
}
.window[data-vertical='true'] .ticker-wrapper {
height: 22rem;
}
.window[data-vertical='true'] [data-row] {
flex-direction: column;
width: 100%;
align-items: stretch;
gap: 1rem;
}
.window[data-vertical='true'] .ticker-track {
gap: 1rem;
padding-block: 0.5rem;
}
.window[data-vertical='true'] .item {
white-space: normal;
}
.story-item {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
align-items: start;
gap: 1rem;
width: 100%;
box-sizing: border-box;
padding: 1rem;
border: 0.0625rem solid var(--border);
border-radius: 1rem;
background: var(--surface);
}
.story-image {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
border: 0.0625rem solid var(--border);
border-radius: 0.625rem;
}
.story-image img {
width: 1.75rem;
height: 1.75rem;
filter: brightness(0.55) saturate(1.3);
}
.story-copy {
display: grid;
min-width: 0;
gap: 0.5rem;
font-size: 0.8125rem;
line-height: 1.6;
}
.story-copy a {
font-size: 0.875rem;
}
@media (prefers-color-scheme: dark) {
.story-image img {
filter: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.ticker-recipe [data-pause] {
display: none;
}
}
</style>
</head>
<body>
<main aria-label="Drag and choose what happens next">
<section class="ticker-recipe">
<h1>Drag and choose what happens next</h1>
<div
class="window"
dir="ltr"
data-vertical="false"
tabindex="0"
role="region"
aria-label="Drag playback demo"
aria-live="off"
>
<div id="ticker-1" data-row>
<span class="item">Grab the strip</span>
<span class="item">Drag to explore</span>
<span class="item">Release to compare</span>
<span class="item">Switch resume on or off</span>
</div>
</div>
<div class="controls" hidden>
<button type="button" data-pause>Pause animation</button>
<button type="button" data-previous>Previous items</button
><button type="button" data-next>Next items</button>
<label
>Position in repeated sequence
<input type="range" data-position min="0" max="1" value="0" /></label
><button
type="button"
data-example-resume
aria-label="Resume after drag"
aria-pressed="true"
>
Resume after drag <span data-resume-state aria-hidden="true">On</span>
</button>
</div>
<p>
With the option on, release and move the pointer away to clear hover pause. With it off,
press Play before trying another drag. Changing the option does not start playback or
clear a manual pause.
</p>
<p role="status" aria-live="polite"></p>
<noscript
><p>Motion needs JavaScript. Scroll to read all the original content.</p></noscript
>
</section>
</main>
<script type="module">
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
const news = ticker('#ticker-1', {
duration: 20,
speed: 40,
direction: 'left',
navigation: true,
resumeAfterDrag: true,
pauseOnHover: true,
})
if (news) {
const lifecycle = new AbortController()
const { signal } = lifecycle
const example = document.querySelector('.ticker-recipe')
const pauseButton = example.querySelector('[data-pause]')
const renderPlayback = () => {
const playing = news.state.requested
example.dataset.paused = String(!playing)
pauseButton.textContent = playing ? 'Pause animation' : 'Play animation'
}
news.on('playback', renderPlayback)
renderPlayback()
example.querySelector('.controls').hidden = false
pauseButton.addEventListener(
'click',
() => {
const playing = news.state.requested
playing ? news.pause() : news.play()
},
{ signal }
)
news.connectControls({
previous: example.querySelector('[data-previous]'),
next: example.querySelector('[data-next]'),
rail: example.querySelector('[data-position]'),
})
const resumeButton = example.querySelector('[data-example-resume]')
resumeButton.addEventListener(
'click',
() => {
const enabled = !news.options.resumeAfterDrag
news.update({ resumeAfterDrag: enabled })
resumeButton.setAttribute('aria-pressed', String(enabled))
resumeButton.querySelector('[data-resume-state]').textContent = enabled ? 'On' : 'Off'
},
{ signal }
)
document.fonts.ready.then(() => {
if (!signal.aborted) {
news.refresh()
}
})
window.addEventListener(
'pagehide',
event => {
if (event.persisted) return
lifecycle.abort()
news.destroy()
},
{ signal }
)
}
</script>
</body>
</html>
These examples show a few possibilities. The next ideas are yours: combine the package’s capabilities with your own content, design and interactions to create the experience your project needs.
New to Ticker? Start with installation Building with an AI agent? Download the Ticker skill Demo brand marks and attribution