Guide

Framework integrations

Use Ticker in React, Vue, Svelte, Astro, Shopify and WordPress with accessible playback controls and explicit client lifecycle ownership.

Ticker uses the same browser API in each host; there is no framework adapter or peer dependency to install. Package / ESM imports ticker() and its stylesheet. HTML / CDN calls window.Ticker.ticker() after loading the standalone script and stylesheet. Keep delivery separate from ownership; see loading terms.

JavaScript and TypeScript

Start with installation. JavaScript uses the same calls without type annotations. TypeScript can import TickerOptions and TickerInstance from the root; use satisfies TickerOptions to check reusable settings. The TypeScript reference lists every exported type.

React, Vue and Svelte

Install Ticker in an existing framework project, save the component and its CSS sibling, then render NewsTicker. These examples include a Pause/Play action that follows requested playback, including reduced-motion or other automatic pauses. They create after client mount and destroy before the component leaves. React’s example also supports its development setup/cleanup cycle.

React

React

NewsTicker.jsx
'use client'

import { useEffect, useRef, useState } from 'react'
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
import './ticker.css'

export const NewsTicker = () => {
  const source = useRef(null)
  const controller = useRef(null)
  const [playing, setPlaying] = useState(true)

  useEffect(() => {
    const news = ticker(source.current, { duration: 20 })
    if (!news) return
    controller.current = news
    const stop = news.on('playback', state => setPlaying(state.requested))
    setPlaying(news.state.requested)
    return () => {
      stop()
      news.destroy()
      controller.current = null
    }
  }, [])

  const toggle = () => {
    const news = controller.current
    if (news?.state.requested) news.pause()
    else news?.play()
  }

  return (
    <section aria-label="Latest news">
      <div ref={source}>
        <span className="ticker-message">Made for the open web</span>
        <span className="ticker-message">Your content, in motion</span>
        <span className="ticker-message">Something good is coming</span>
      </div>
      <button type="button" onClick={toggle}>
        {playing ? 'Pause' : 'Play'}
      </button>
    </section>
  )
}

CSS

ticker.css
.ticker-message {
  display: inline-block;
  padding: 0.75rem 1.25rem;
}

Vue

Vue

NewsTicker.vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { ticker } from '@samline/ticker'
import '@samline/ticker/style.css'
import './ticker.css'

const source = ref(null)
const playing = ref(true)
let news
let stop
onMounted(() => {
  news = ticker(source.value, { duration: 20 })
  if (!news) return
  playing.value = news.state.requested
  stop = news.on('playback', state => {
    playing.value = state.requested
  })
})
onUnmounted(() => {
  stop?.()
  news?.destroy()
})
const toggle = () => {
  if (news?.state.requested) news.pause()
  else news?.play()
}
</script>

<template>
  <section aria-label="Latest news">
    <div ref="source">
      <span class="ticker-message">Made for the open web</span>
      <span class="ticker-message">Your content, in motion</span>
      <span class="ticker-message">Something good is coming</span>
    </div>
    <button type="button" @click="toggle">{{ playing ? 'Pause' : 'Play' }}</button>
  </section>
</template>

CSS

ticker.css
.ticker-message {
  display: inline-block;
  padding: 0.75rem 1.25rem;
}

Svelte

Svelte

NewsTicker.svelte
<script>
  import { onMount } from 'svelte'
  import { ticker } from '@samline/ticker'
  import '@samline/ticker/style.css'
  import './ticker.css'

  let source
  let news
  let playing = $state(true)
  onMount(() => {
    news = ticker(source, { duration: 20 })
    if (!news) return
    playing = news.state.requested
    const stop = news.on('playback', state => { playing = state.requested })
    return () => { stop(); news.destroy() }
  })
  const toggle = () => { if (news?.state.requested) news.pause(); else news?.play() }
</script>

<section aria-label="Latest news">
  <div bind:this={source}>
    <span class="ticker-message">Made for the open web</span>
    <span class="ticker-message">Your content, in motion</span>
    <span class="ticker-message">Something good is coming</span>
  </div>
  <button type="button" onclick={toggle}>{playing ? 'Pause' : 'Play'}</button>
</section>

CSS

ticker.css
.ticker-message {
  display: inline-block;
  padding: 0.75rem 1.25rem;
}

The enhanced source DOM belongs to Ticker until destroy() restores it. Keep this subtree stable while mounted: use a component ref for the controller, update() for settings, and refresh() after application-owned source edits. Do not replace the runtime’s track/clones from a framework render. For reactive item lists, tear down before the framework replaces the original nodes, then initialize the new source. Do not place framework components in generated clones.

The lifecycle hooks follow React Effects, Vue mount/unmount hooks and Svelte onMount cleanup.

Astro

Import and call ticker() inside a processed client <script>, not frontmatter. The first integration is ordinary browser JavaScript and needs no hydrated framework component. With Astro client navigation, initialize the current page after astro:page-load and destroy the owning instances on astro:before-swap; keep document listeners registered once. Full-page examples preserve instances on persisted pagehide and clean up on a real departure.

Shopify

Use a theme section and theme assets. Copy the published 2.1.0 dist/browser/global.global.js as assets/ticker.global.js and dist/style.css as assets/ticker.css; save the JavaScript and CSS below beside them. Add the loader once in layout/theme.liquid, then add sections/ticker-strip.liquid in the theme editor. The section itself contains no extra runtime script tags.

Section

sections/ticker-strip.liquid
<section data-ticker-widget aria-label="Latest news">
  <div data-source>
    <span class="ticker-message">Made for the open web</span>
    <span class="ticker-message">Your content, in motion</span>
    <span class="ticker-message">Something good is coming</span>
  </div>
  <button type="button" data-toggle disabled>Pause</button>
</section>

{% schema %}
{"name":"Ticker example","presets":[{"name":"Ticker example"}]}
{% endschema %}

Layout

layout/theme.liquid
{{ 'ticker.css' | asset_url | stylesheet_tag }}
{{ 'integration.css' | asset_url | stylesheet_tag }}
<script src="{{ 'ticker.global.js' | asset_url }}" defer></script>
<script src="{{ 'integration.js' | asset_url }}" defer></script>

JavaScript

assets/integration.js
const instances = new Map()
const mount = scope => {
  scope.querySelectorAll('[data-ticker-widget]').forEach(root => {
    if (instances.has(root)) return
    const button = root.querySelector('[data-toggle]')
    const news = window.Ticker.ticker(root.querySelector('[data-source]'), { duration: 20 })
    if (!news) return
    const events = new AbortController()
    const render = () => {
      button.textContent = news.state.requested ? 'Pause' : 'Play'
    }
    const stop = news.on('playback', render)
    render()
    button.disabled = false
    button.addEventListener(
      'click',
      () => {
        if (news.state.requested) news.pause()
        else news.play()
      },
      { signal: events.signal }
    )
    instances.set(root, () => {
      events.abort()
      stop()
      news.destroy()
      button.disabled = true
    })
  })
}
const unmount = scope => {
  for (const [root, dispose] of instances) {
    if (scope.contains(root)) {
      dispose()
      instances.delete(root)
    }
  }
}
mount(document)
document.addEventListener('shopify:section:load', event => mount(event.target))
document.addEventListener('shopify:section:unload', event => unmount(event.target))
window.addEventListener('pagehide', event => {
  if (!event.persisted) unmount(document)
})

CSS

assets/integration.css
.ticker-message {
  display: inline-block;
  padding: 0.75rem 1.25rem;
}

The loader creates independent instances for repeated sections, handles shopify:section:load, and destroys only the removed section on shopify:section:unload. Static content remains readable without JavaScript. Shopify’s section lifecycle and asset loading describe this host contract.

WordPress

Create wp-content/plugins/ticker-example/ with the plugin PHP file below and an assets/ folder. Save the same pinned runtime files there as ticker.global.js and ticker.css, plus the provided integration.js and integration.css. Activate Ticker example, then put [ticker_strip] in a Shortcode block on a normal post or page.

Plugin

ticker-example.php
<?php
/**
 * Plugin Name: Ticker example
 */
add_action('wp_enqueue_scripts', static function () {
    $post = get_post();
    if (!is_singular() || !$post || !has_shortcode($post->post_content, 'ticker_strip')) return;
    $assets = plugin_dir_url(__FILE__) . 'assets/';
    wp_enqueue_style('ticker-runtime', $assets . 'ticker.css', [], '2.1.0');
    wp_enqueue_style('ticker-example', $assets . 'integration.css', ['ticker-runtime'], '1.0.0');
    wp_enqueue_script('ticker-runtime', $assets . 'ticker.global.js', [], '2.1.0', ['strategy' => 'defer', 'in_footer' => true]);
    wp_enqueue_script('ticker-example', $assets . 'integration.js', ['ticker-runtime'], '1.0.0', ['strategy' => 'defer', 'in_footer' => true]);
});
add_shortcode('ticker_strip', static fn () => '<section data-ticker-widget aria-label="Latest news">
  <div data-source>
    <span class="ticker-message">Made for the open web</span>
    <span class="ticker-message">Your content, in motion</span>
    <span class="ticker-message">Something good is coming</span>
  </div>
  <button type="button" data-toggle disabled>Pause</button>
</section>');

JavaScript

assets/integration.js
const instances = new Map()
const mount = scope => {
  scope.querySelectorAll('[data-ticker-widget]').forEach(root => {
    if (instances.has(root)) return
    const button = root.querySelector('[data-toggle]')
    const news = window.Ticker.ticker(root.querySelector('[data-source]'), { duration: 20 })
    if (!news) return
    const events = new AbortController()
    const render = () => {
      button.textContent = news.state.requested ? 'Pause' : 'Play'
    }
    const stop = news.on('playback', render)
    render()
    button.disabled = false
    button.addEventListener(
      'click',
      () => {
        if (news.state.requested) news.pause()
        else news.play()
      },
      { signal: events.signal }
    )
    instances.set(root, () => {
      events.abort()
      stop()
      news.destroy()
      button.disabled = true
    })
  })
}
const unmount = scope => {
  for (const [root, dispose] of instances) {
    if (scope.contains(root)) {
      dispose()
      instances.delete(root)
    }
  }
}
mount(document)
window.addEventListener('pagehide', event => {
  if (!event.persisted) unmount(document)
})

CSS

assets/integration.css
.ticker-message {
  display: inline-block;
  padding: 0.75rem 1.25rem;
}

This example targets WordPress 6.3+ deferred-script arguments. Runtime and integration scripts are queued with explicit dependencies, so markup is present before initialization. The sample loads assets for ordinary singular content containing the shortcode; widgets, page builders and AJAX navigation need their own enqueue/mount policy. Do not paste a second IIFE into each shortcode. See wp_enqueue_script and frontend asset hooks.

Ownership and accessibility

Keep original content meaningful and provide a pause action for moving content. Ticker owns clones, measurements, reduced-motion handling and automatic pause causes. Each host owns its original DOM, labels, subscriptions and teardown. Closing a page widget is not permission to destroy unrelated registry entries; see lifecycle ownership and accessibility.