Ticker Tape Effect
A canvas-based ticker-tape parade. Curled paper strips catch the light, sway, tumble, and fade - full-page or container-scoped, with theme-aware palettes.
Domma.effects.tickerTape()
Drops thin coloured rectangles from the top of a container - or the entire viewport - as if released from a stack of windows during a ticker-tape parade. Each strip falls, sways gently from side to side, rotates as it tumbles, and fades to nothing before reaching the bottom. Pass null for a full-page overlay, or a selector for a scoped effect.
// Full-page overlay using current theme colours
const ticker = Domma.effects.tickerTape(null);
// Container-scoped, festive palette, slower fall
Domma.effects.tickerTape('#celebration', {
palette: 'festive',
density: 60,
speed: 0.7
});
// One-shot burst - strips arrive and the effect ends naturally
Domma.effects.tickerTape('#stage', {
burst: true,
burstCount: 200,
palette: 'gold'
});
// Control methods
ticker.pause();
ticker.resume();
ticker.stop();
ticker.restart();
ticker.destroy();
Options
| Option | Type | Default | Description |
|---|---|---|---|
palette |
string | string[] |
'theme' |
Named palette or custom colour array. 'theme' reads CSS variables from the current Domma theme. |
density |
number |
50 |
Average number of strips on screen at any moment |
speed |
number |
1 |
Fall-speed multiplier (lower = slower drift) |
sway |
number |
60 |
Horizontal sway amplitude in pixels |
rotationSpeed |
number |
6 |
Maximum rotation in degrees per frame |
minWidth / maxWidth |
number |
5 / 9 |
Strip width range in pixels |
minHeight / maxHeight |
number |
12 / 22 |
Strip height range in pixels |
fadeStart |
number |
0.55 |
Fraction of the fall (0-1) at which fade begins |
burst |
boolean |
false |
If true, drops a single batch and stops respawning |
burstCount |
number |
150 |
Strips emitted when burst: true |
zIndex |
number |
1 |
Canvas stacking order |
respectMotionPreference |
boolean |
true |
Honour prefers-reduced-motion OS setting |
Built-in Palettes
Pass any of these names as the palette option - or supply your own array of colour strings.
<div class="palette-swatches" id="palette-preview"></div>/**
* Built-in Palettes - from public/showcase/effects/ticker-tape.
*/
export function mount(root) {
// ── Render palette swatches ──────────────────────────────────────────
const palettes = {
theme: ['var(--dm-primary)', 'var(--dm-success)', 'var(--dm-warning)', 'var(--dm-danger)', 'var(--dm-info)'],
rainbow: ['#ef4444', '#f97316', '#facc15', '#22c55e', '#0ea5e9', '#6366f1', '#a855f7', '#ec4899'],
festive: ['#dc2626', '#16a34a', '#fbbf24', '#3b82f6', '#a855f7', '#ec4899'],
gold: ['#fbbf24', '#f59e0b', '#fde68a', '#fffbeb', '#d97706', '#fcd34d'],
silver: ['#e5e7eb', '#cbd5e1', '#94a3b8', '#f8fafc', '#9ca3af', '#d1d5db'],
pastel: ['#fbcfe8', '#bae6fd', '#bbf7d0', '#fef3c7', '#ddd6fe', '#fed7aa'],
mono: ['#1f2937', '#4b5563', '#9ca3af', '#d1d5db', '#f3f4f6'],
sunset: ['#f43f5e', '#fb923c', '#facc15', '#f59e0b', '#ec4899'],
ocean: ['#0ea5e9', '#22d3ee', '#06b6d4', '#3b82f6', '#6366f1'],
forest: ['#16a34a', '#65a30d', '#84cc16', '#22c55e', '#15803d'],
bridal: ['#ffffff', '#fef3c7', '#fce7f3', '#fbcfe8', '#f9a8d4']
};
function renderPaletteCard(name, isPicker) {
const stripes = palettes[name].map(c => `<span style="background:${c}"></span>`).join('');
return `
<div class="palette-card${(isPicker && name === 'theme') ? ' active' : ''}" data-palette="${name}">
<div class="palette-name">${name}</div>
<div class="palette-stripes">${stripes}</div>
</div>`;
}
// Note: pass {safe: false} because Domma's default html() sanitiser strips
// both inline `style` attributes and `data-*` attributes - we'd lose the
// swatch colours AND the data-palette hook the click handler reads.
const previewMarkup = Object.keys(palettes).map(n => renderPaletteCard(n, false)).join('');
$(root).find('#palette-preview').html(previewMarkup, { safe: false });
}Demo: Theme-Aware Palette
The default palette resolves the active theme's CSS variables, so the same call produces different colours under different themes. Try switching the theme variant in the toolbar above.
<div class="tape-container" id="tape-container-1">
<div class="tape-container-label">
<div style="font-size: 2rem; margin-bottom: 0.5rem;">🎉</div>
<div>Press Start to release the tape</div>
</div>
</div>
<span id="status-1" class="status-badge stopped"><span class="status-dot"></span><span class="status-text">Stopped</span></span>
<button type="button" class="btn btn-primary" id="start-1">Start</button>
<button type="button" class="btn btn-secondary" id="pause-1">Pause</button>
<button type="button" class="btn btn-secondary" id="resume-1">Resume</button>
<button type="button" class="btn btn-secondary" id="restart-1">Restart</button>
<button type="button" class="btn btn-danger" id="destroy-1">Destroy</button>/**
* Demo: Theme-Aware Palette - from public/showcase/effects/ticker-tape.
*/
export function mount(root) {
// ── Status badge helper ───────────────────────────────────────────────
function setStatus($badge, state) {
if (!$badge || !$badge.length) return;
const labels = { running: 'Running', paused: 'Paused', stopped: 'Stopped' };
$badge.attr('class', 'status-badge ' + state);
$badge.find('.status-dot')
.attr('class', 'status-dot' + (state === 'running' ? ' pulse-dot' : ''));
$badge.find('.status-text').text(labels[state] || state);
}
// ── Demo 1: Theme-aware container ────────────────────────────────────
let ctrl1 = null;
const badge1 = $(root).find('#status-1');
$(root).find('#start-1').on('click', () => {
if (ctrl1) ctrl1.destroy();
ctrl1 = Domma.effects.tickerTape('#tape-container-1', {
palette: 'theme',
density: 50,
respectMotionPreference: false
});
setStatus(badge1, 'running');
});
$(root).find('#pause-1').on('click', () => { if (ctrl1) { ctrl1.pause(); setStatus(badge1, 'paused'); } });
$(root).find('#resume-1').on('click', () => { if (ctrl1) { ctrl1.resume(); setStatus(badge1, 'running'); } });
$(root).find('#restart-1').on('click', () => { if (ctrl1) { ctrl1.restart(); setStatus(badge1, 'running'); } });
$(root).find('#destroy-1').on('click', () => { if (ctrl1) { ctrl1.destroy(); ctrl1 = null; setStatus(badge1, 'stopped'); } });
}const ticker = Domma.effects.tickerTape('#tape-container', {
palette: 'theme', // pull from --dm-primary, --dm-success, etc.
density: 50,
speed: 1
});
Demo: Choose a Palette
Click a palette to swap colours instantly.
<div class="palette-swatches" id="palette-picker"></div>
<div class="tape-container mt-3" id="tape-container-2" style="min-height: 280px;"></div>/**
* Demo: Choose a Palette - from public/showcase/effects/ticker-tape.
*/
export function mount(root) {
// ── Render palette swatches ──────────────────────────────────────────
const palettes = {
theme: ['var(--dm-primary)', 'var(--dm-success)', 'var(--dm-warning)', 'var(--dm-danger)', 'var(--dm-info)'],
rainbow: ['#ef4444', '#f97316', '#facc15', '#22c55e', '#0ea5e9', '#6366f1', '#a855f7', '#ec4899'],
festive: ['#dc2626', '#16a34a', '#fbbf24', '#3b82f6', '#a855f7', '#ec4899'],
gold: ['#fbbf24', '#f59e0b', '#fde68a', '#fffbeb', '#d97706', '#fcd34d'],
silver: ['#e5e7eb', '#cbd5e1', '#94a3b8', '#f8fafc', '#9ca3af', '#d1d5db'],
pastel: ['#fbcfe8', '#bae6fd', '#bbf7d0', '#fef3c7', '#ddd6fe', '#fed7aa'],
mono: ['#1f2937', '#4b5563', '#9ca3af', '#d1d5db', '#f3f4f6'],
sunset: ['#f43f5e', '#fb923c', '#facc15', '#f59e0b', '#ec4899'],
ocean: ['#0ea5e9', '#22d3ee', '#06b6d4', '#3b82f6', '#6366f1'],
forest: ['#16a34a', '#65a30d', '#84cc16', '#22c55e', '#15803d'],
bridal: ['#ffffff', '#fef3c7', '#fce7f3', '#fbcfe8', '#f9a8d4']
};
function renderPaletteCard(name, isPicker) {
const stripes = palettes[name].map(c => `<span style="background:${c}"></span>`).join('');
return `
<div class="palette-card${(isPicker && name === 'theme') ? ' active' : ''}" data-palette="${name}">
<div class="palette-name">${name}</div>
<div class="palette-stripes">${stripes}</div>
</div>`;
}
const pickerMarkup = Object.keys(palettes).map(n => renderPaletteCard(n, true)).join('');
$(root).find('#palette-picker').html(pickerMarkup, { safe: false });
// ── Demo 2: Palette picker ───────────────────────────────────────────
let ctrl2 = Domma.effects.tickerTape('#tape-container-2', {
palette: 'theme',
density: 50,
respectMotionPreference: false
});
$(root).find('#palette-picker').on('click', (e) => {
const card = e.target.closest('.palette-card');
if (!card) return;
$('#palette-picker .palette-card').removeClass('active');
card.classList.add('active');
const name = card.getAttribute('data-palette');
if (ctrl2) ctrl2.destroy();
ctrl2 = Domma.effects.tickerTape('#tape-container-2', {
palette: name,
density: 50,
respectMotionPreference: false
});
});
}Demo: Tune the Physics
Adjust the controls and click Apply.
<label>Density</label>
<input type="range" id="opt-density" min="10" max="200" value="60">
<span id="val-density">60</span>
<label>Speed</label>
<input type="range" id="opt-speed" min="0.2" max="3" step="0.1" value="1">
<span id="val-speed">1</span>
<label>Sway</label>
<input type="range" id="opt-sway" min="0" max="200" step="5" value="60">
<span id="val-sway">60</span>
<label>Rotation</label>
<input type="range" id="opt-rotation" min="0" max="20" step="0.5" value="6">
<span id="val-rotation">6</span>
<label>Fade Start</label>
<input type="range" id="opt-fade" min="0.1" max="0.95" step="0.05" value="0.55">
<span id="val-fade">0.55</span>
<div class="tape-container mt-4" id="tape-container-3"></div>
<button type="button" class="btn btn-primary" id="apply-3">Apply</button>
<button type="button" class="btn btn-danger" id="destroy-3">Destroy</button>/**
* Demo: Tune the Physics - from public/showcase/effects/ticker-tape.
*/
export function mount(root) {
// ── Demo 3: Customisation ────────────────────────────────────────────
const sliders = ['density', 'speed', 'sway', 'rotation', 'fade'];
sliders.forEach(name => {
$(`#opt-${name}`).on('input', (e) => {
$(`#val-${name}`).text(e.target.value);
});
});
let ctrl3 = null;
function applyDemo3() {
if (ctrl3) ctrl3.destroy();
ctrl3 = Domma.effects.tickerTape('#tape-container-3', {
palette: 'rainbow',
density: parseInt($(root).find('#opt-density').val(), 10),
speed: parseFloat($(root).find('#opt-speed').val()),
sway: parseInt($(root).find('#opt-sway').val(), 10),
rotationSpeed: parseFloat($(root).find('#opt-rotation').val()),
fadeStart: parseFloat($(root).find('#opt-fade').val()),
respectMotionPreference: false
});
}
$(root).find('#apply-3').on('click', applyDemo3);
$(root).find('#destroy-3').on('click', () => { if (ctrl3) { ctrl3.destroy(); ctrl3 = null; } });
applyDemo3();
}Demo: One-Shot Burst
In burst mode, a single batch of strips is released and the effect winds down naturally - perfect for confirmation moments (form submission, achievement unlocked, sale completed).
<div class="tape-container" id="tape-container-4">
<div class="tape-container-label">
<div style="font-size: 2rem; margin-bottom: 0.5rem;">🎊</div>
<div>Click "Celebrate" for a one-shot burst</div>
</div>
</div>
<button type="button" class="btn btn-primary" id="burst-4">Celebrate!</button>/**
* Demo: One-Shot Burst - from public/showcase/effects/ticker-tape.
*/
export function mount(root) {
// ── Demo 4: Burst ────────────────────────────────────────────────────
$(root).find('#burst-4').on('click', () => {
Domma.effects.tickerTape('#tape-container-4', {
palette: 'gold',
burst: true,
burstCount: 220,
speed: 1.4,
respectMotionPreference: false
});
});
}Domma.effects.tickerTape('#stage', {
burst: true,
burstCount: 200,
palette: 'gold',
speed: 1.4
});
Demo: Full-Page Overlay
Pass null as the selector to drop tape across the whole viewport. Pieces sit behind page content via pointer-events: none.
<span id="status-5" class="status-badge stopped"><span class="status-dot"></span><span class="status-text">Stopped</span></span>
<button type="button" class="btn btn-primary" id="start-5">Start Full-Page</button>
<button type="button" class="btn btn-secondary" id="pause-5">Pause</button>
<button type="button" class="btn btn-secondary" id="resume-5">Resume</button>
<button type="button" class="btn btn-danger" id="destroy-5">Destroy</button>/**
* Demo: Full-Page Overlay - from public/showcase/effects/ticker-tape.
*/
export function mount(root) {
// ── Status badge helper ───────────────────────────────────────────────
function setStatus($badge, state) {
if (!$badge || !$badge.length) return;
const labels = { running: 'Running', paused: 'Paused', stopped: 'Stopped' };
$badge.attr('class', 'status-badge ' + state);
$badge.find('.status-dot')
.attr('class', 'status-dot' + (state === 'running' ? ' pulse-dot' : ''));
$badge.find('.status-text').text(labels[state] || state);
}
// ── Demo 5: Full-page overlay ───────────────────────────────────────
let ctrl5 = null;
const badge5 = $(root).find('#status-5');
$(root).find('#start-5').on('click', () => {
if (ctrl5) ctrl5.destroy();
ctrl5 = Domma.effects.tickerTape(null, {
palette: 'rainbow',
density: 80,
zIndex: 9999,
respectMotionPreference: false
});
setStatus(badge5, 'running');
});
$(root).find('#pause-5').on('click', () => { if (ctrl5) { ctrl5.pause(); setStatus(badge5, 'paused'); } });
$(root).find('#resume-5').on('click', () => { if (ctrl5) { ctrl5.resume(); setStatus(badge5, 'running'); } });
$(root).find('#destroy-5').on('click', () => { if (ctrl5) { ctrl5.destroy(); ctrl5 = null; setStatus(badge5, 'stopped'); } });
// The full-page effect goes with the demo: stopped when it is reset or left.
return () => { if (ctrl5) ctrl5.destroy(); };
}Domma.effects.tickerTape(null, {
palette: 'rainbow',
density: 80,
zIndex: 9999
});