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.

Built-in Palettes
<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.

Demo: Theme-Aware Palette
🎉
Press Start to release the tape
Stopped
<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.

Demo: Choose a Palette
<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.

Demo: Tune the Physics
60 1 60 6 0.55
<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).

Demo: One-Shot Burst
🎊
Click "Celebrate" for a one-shot burst
<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.

Demo: Full-Page Overlay
Stopped
<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
});