A time-synchronized marquee engine for multi-monitor displays. Each browser window renders an identical marquee animation, staying perfectly in sync using a shared clock via BroadcastChannel.
- Features
- Installation
- Quick Start
- Multi-Monitor Setup
- API Reference
- How It Works
- Browser Support
- Limitations
- License
- Time-based synchronization - All windows compute position from a shared start time
- Multi-monitor support - Configure window index and width for seamless cross-monitor continuity
- No framework dependencies - Pure ES modules with zero runtime dependencies
- Shadow DOM encapsulation - Styles don't leak, content is cleanly isolated
- ResizeObserver integration - Automatically adapts to content changes
- Reduced motion support - Respects
prefers-reduced-motionuser preference - Fallback support - Uses localStorage events when BroadcastChannel is unavailable
- TypeScript-friendly - Full JSDoc type annotations included
# Clone the repository
git clone <repo-url>
cd marqueex
# Install dependencies
npm install
# Run the demo
npm run start
# Open http://localhost:3000/demo/Or import directly in your project:
import { MarqueeEngine } from './src/index.js';<div id="marquee">
<span>Your scrolling content here</span>
</div>
<script type="module">
import { MarqueeEngine } from './src/index.js';
const engine = new MarqueeEngine({
container: document.getElementById('marquee'),
speed: 100, // pixels per second
gap: 50, // gap between content repetitions
sync: true // enable cross-window sync
});
engine.start();
</script>For true multi-monitor continuity, assign each window a position index:
// Window on leftmost monitor
const engine = new MarqueeEngine({
container: document.getElementById('marquee'),
windowIndex: 0, // 0 = leftmost position
windowWidth: 1920 // width of each monitor in pixels
});
// Window on second monitor (opened separately)
const engine = new MarqueeEngine({
container: document.getElementById('marquee'),
windowIndex: 1, // 1 = second from left
windowWidth: 1920
});- Open the demo page in your first browser window
- Click "Open New Window" or manually open the same URL in additional windows
- Position each window on a different monitor
- All marquees will animate in sync showing the same content
For a seamless effect where content appears to scroll from one monitor to the next:
- Configure each window with its
windowIndex(0, 1, 2, etc. from left to right) - Set
windowWidthto match your monitor resolution - Each window will show a different portion of the scrolling content
// Example: 3-monitor setup with 1920px monitors
const params = new URLSearchParams(window.location.search);
const windowIndex = parseInt(params.get('monitor') || '0');
const engine = new MarqueeEngine({
container: document.getElementById('marquee'),
windowIndex: windowIndex,
windowWidth: 1920
});Note: True pixel-perfect continuity across monitors depends on DPI, zoom levels, and window positioning. This library provides time-based synchronization which creates a visually convincing effect.
The main animation controller.
| Option | Type | Default | Description |
|---|---|---|---|
container |
HTMLElement |
required | The marquee container element |
speed |
number |
100 |
Animation speed in pixels per second |
gap |
number |
50 |
Gap between content repetitions in pixels |
sync |
boolean |
true |
Enable cross-window synchronization |
resyncInterval |
number |
0 |
Resync broadcast interval in ms (0 = disabled) |
respectReducedMotion |
boolean |
true |
Respect prefers-reduced-motion |
windowIndex |
number |
0 |
Position of this window (0 = leftmost, 1 = next, etc.) |
windowWidth |
number |
0 |
Width of each monitor in pixels (0 = auto-detect from viewport) |
| Method | Description |
|---|---|
start() |
Starts the animation and broadcasts to other windows |
stop() |
Stops the animation and broadcasts to other windows |
sync(startTime) |
Manually syncs to a specific start time (ms) |
destroy() |
Cleans up all resources (observers, channels, timers) |
engine.start(); // Start animation
engine.stop(); // Stop animation
engine.sync(startTime); // Sync to timestamp
engine.destroy(); // Clean up| Property | Type | Access | Description |
|---|---|---|---|
speed |
number |
get/set | Animation speed in pixels per second |
gap |
number |
get/set | Gap between content repetitions in pixels |
windowIndex |
number |
get/set | Position index of this window (0-based) |
windowWidth |
number |
get/set | Monitor width in pixels (0 = auto) |
running |
boolean |
get | Whether the animation is currently running |
isAuthority |
boolean |
get | Whether this instance is the time authority |
startTime |
number |
get | Current shared start time in milliseconds |
reducedMotionActive |
boolean |
get | Whether reduced motion preference is active |
// Reading properties
console.log(engine.running); // true/false
console.log(engine.isAuthority); // true/false
// Setting properties (automatically broadcasts to other windows)
engine.speed = 150; // Change speed
engine.gap = 100; // Change gap
engine.windowIndex = 2; // Change window position
engine.windowWidth = 1920; // Set monitor widthLow-level BroadcastChannel wrapper for custom synchronization needs.
const channel = new SyncChannel(channelName);| Parameter | Type | Default | Description |
|---|---|---|---|
channelName |
string |
'marquee-sync' |
Name of the broadcast channel |
| Method | Description |
|---|---|
broadcast(message) |
Sends a message to all other windows |
onMessage(callback) |
Registers a callback for incoming messages |
requestSync() |
Requests sync from existing instances |
destroy() |
Closes the channel and cleans up |
| Property | Type | Description |
|---|---|---|
isUsingFallback |
boolean |
Whether localStorage fallback is active |
The SyncChannel uses these message types internally:
| Type | Description |
|---|---|
INIT |
Initial sync message with start time and config |
RESYNC |
Periodic resync from the authority |
REQUEST_SYNC |
Request sync from existing instances |
CONFIG |
Configuration change (speed, gap, windowWidth) |
PLAYBACK |
Playback state change (start/stop) |
import { SyncChannel, getCurrentTime } from './src/index.js';
const channel = new SyncChannel('my-custom-channel');
// Listen for messages
channel.onMessage((msg) => {
console.log('Received:', msg.type, msg);
if (msg.type === 'INIT') {
// Sync to the shared start time
myAnimation.setStartTime(msg.startTime);
}
});
// Request sync from existing instances
channel.requestSync();
// Or become the authority and broadcast
channel.broadcast({
type: 'INIT',
startTime: getCurrentTime(),
speed: 100,
gap: 50
});
// Check if using localStorage fallback
if (channel.isUsingFallback) {
console.log('BroadcastChannel not available, using localStorage');
}
// Clean up when done
channel.destroy();Returns the current high-precision timestamp using performance.timeOrigin + performance.now().
import { getCurrentTime } from './src/index.js';
const timestamp = getCurrentTime(); // e.g., 1706745600000.123- The first instance becomes the time authority and broadcasts an
INITmessage with the start time - New instances send
REQUEST_SYNCand wait for anINITresponse - All instances compute animation offset using the same formula:
// Base offset from time
offset = ((currentTime - startTime) / 1000) * speed
// Add window position offset for multi-monitor continuity
totalOffset = offset + (windowIndex * windowWidth)
// Wrap for seamless looping
wrappedOffset = totalOffset % contentWidthThis makes synchronization deterministic without per-frame messaging.
┌─────────────────────┐ ┌─────────────────────┐
│ Window 1 │ │ Window 2 │
│ (windowIndex: 0) │ │ (windowIndex: 1) │
│ ┌───────────────┐ │ │ ┌───────────────┐ │
│ │ MarqueeEngine │ │ │ │ MarqueeEngine │ │
│ │ (Authority) │ │ │ │ (Follower) │ │
│ └───────┬───────┘ │ │ └───────┬───────┘ │
│ │ │ │ │ │
│ ┌───────▼───────┐ │ │ ┌───────▼───────┐ │
│ │ RAF Loop │ │ │ │ RAF Loop │ │
│ │ offset + 0px │ │ │ │ offset+1920px │ │
│ └───────┬───────┘ │ │ └───────┬───────┘ │
└──────────┼──────────┘ └──────────┼──────────┘
│ │
│ ┌─────────────────────┐ │
└──│ BroadcastChannel │──┘
│ (startTime, config) │
└─────────────────────┘
Window 1 (first to load) Window 2 (loads later)
│ │
│ ◄──── REQUEST_SYNC ────────────┤
│ │
├────── INIT ───────────────────►│
│ (startTime, speed, │
│ gap, running) │
│ │
│ ◄──── CONFIG ──────────────────┤
│ (user changes speed) │
│ │
| Browser | Version | Notes |
|---|---|---|
| Chrome | 54+ | Full support |
| Edge | 79+ | Full support |
| Firefox | 38+ | Full support |
| Safari | 15.4+ | Full support |
| Older browsers | - | Falls back to localStorage events |
The library automatically falls back to localStorage events for older browsers or when BroadcastChannel is unavailable (e.g., private browsing mode in some browsers).
- No monitor detection - Cannot detect monitor arrangement or geometry;
windowIndexmust be set manually - Pixel alignment - Exact pixel alignment depends on DPI, zoom, and window positioning
- Same origin only - Cross-device synchronization is not supported (requires same origin)
- No SSR - Requires DOM (browser environment only)
- Clock drift - Very long-running animations may experience minor drift between windows
MIT