Butterflies Effect
A canvas effect where procedurally drawn butterflies wander and rise with flapping wings - full-page or container-scoped, with theme-aware palettes.
Domma.effects.butterflies()
Releases procedurally drawn butterflies that drift up through a container - or the entire viewport - as if crossing a meadow on a spring afternoon. Each one wanders along a gently bobbing path, beats its wings, and slips away off the top edge. Pass null for a full-page overlay, or a selector for a scoped effect. Colours can follow the active theme or any of the built-in palettes.
// Full-page overlay using the default meadow palette
const flutter = Domma.effects.butterflies(null);
// Container-scoped, pastel palette, denser swarm
Domma.effects.butterflies('#hero', { palette: 'pastel', density: 24 });
// One-shot release - a burst of butterflies that winds down naturally
Domma.effects.butterflies('#card', { burst: true, burstCount: 30 });
// Control methods
flutter.pause();
flutter.resume();
flutter.stop();
flutter.restart();
flutter.destroy();
Options
| Option | Type | Default | Description |
|---|---|---|---|
palette |
string | string[] |
'meadow' |
Named palette or custom colour array. 'theme' reads CSS variables from the current Domma theme. |
density |
number |
18 |
Average number of butterflies on screen at any moment |
speed |
number |
1 |
Overall flight-speed multiplier |
wander |
number |
1 |
How much each butterfly meanders from a straight path |
riseSpeed |
number |
0.4 |
Upward drift multiplier (higher = quicker rise) |
flapSpeed |
number |
1 |
Wing-flap rate multiplier |
minSize |
number |
14 |
Smallest butterfly wingspan in pixels |
maxSize |
number |
26 |
Largest butterfly wingspan in pixels |
twoTone |
boolean |
true |
Give each butterfly two palette colours - one for the upper wings, one for the lower |
burst |
boolean |
false |
If true, releases a single batch and stops respawning |
burstCount |
number |
40 |
Butterflies 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/butterflies.
*/
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)'],
meadow: ['#84cc16', '#a3e635', '#bbf7d0', '#bae6fd', '#fef08a', '#ddd6fe'],
pastel: ['#fbcfe8', '#bae6fd', '#bbf7d0', '#fef3c7', '#ddd6fe', '#fed7aa'],
rainbow: ['#ef4444', '#f97316', '#facc15', '#22c55e', '#0ea5e9', '#6366f1', '#a855f7', '#ec4899'],
sunset: ['#f43f5e', '#fb923c', '#facc15', '#f59e0b', '#ec4899']
};
function renderPaletteCard(name, isPicker) {
const stripes = palettes[name].map(c => `<span style="background:${c}"></span>`).join('');
return `
<div class="palette-card${(isPicker && name === 'meadow') ? ' 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 });
}How It Works
Each butterfly spawns just off the bottom edge - or low on the left and right sides - and is given a random colour from the chosen palette, a wingspan between minSize and maxSize, and a starting heading.
Every so often it picks a fresh heading that is biased gently upward, then eases toward it rather than snapping across. That gradual turning is what gives the flight its characteristic bob and wander: the butterfly is always chasing a target direction it never quite settles on. The wander option widens how far those targets stray from straight up, while riseSpeed controls how strongly the upward bias pulls.
The wings are drawn fresh each frame. A running flapPhase is advanced by flapSpeed, and Math.sin(flapPhase) drives the wing spread - wide open at the top of the beat, folded at the bottom - so the flap reads as a continuous, lifelike motion rather than a two-frame toggle.
As a butterfly drifts past the top of the canvas it fades and is retired. In continuous mode a replacement spawns from below to keep the swarm at roughly density members; in burst mode no replacements are made, so the flutter thins out and ends on its own.
Demo: Theme / Meadow Palette
The default meadow palette gives soft spring greens and skies. Press Start to release a wandering swarm into the container below.
<div class="butterfly-container" id="butterfly-container-1">
<div class="butterfly-container-label">
<div style="font-size: 2rem; margin-bottom: 0.5rem;">🦋</div>
<div>Press Start to release the butterflies</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 / Meadow Palette - from public/showcase/effects/butterflies.
*/
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 / meadow container ─────────────────────────────────
let ctrl1 = null;
const badge1 = $(root).find('#status-1');
$(root).find('#start-1').on('click', () => {
if (ctrl1) ctrl1.destroy();
ctrl1 = Domma.effects.butterflies('#butterfly-container-1', {
palette: 'meadow',
density: 18,
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 flutter = Domma.effects.butterflies('#butterfly-container', {
palette: 'meadow', // soft spring greens and skies
density: 18,
speed: 1
});
Demo: Choose a Palette
Click a palette to swap colours instantly.
<div class="palette-swatches" id="palette-picker"></div>
<div class="butterfly-container mt-3" id="butterfly-container-2" style="min-height: 280px;"></div>/**
* Demo: Choose a Palette - from public/showcase/effects/butterflies.
*/
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)'],
meadow: ['#84cc16', '#a3e635', '#bbf7d0', '#bae6fd', '#fef08a', '#ddd6fe'],
pastel: ['#fbcfe8', '#bae6fd', '#bbf7d0', '#fef3c7', '#ddd6fe', '#fed7aa'],
rainbow: ['#ef4444', '#f97316', '#facc15', '#22c55e', '#0ea5e9', '#6366f1', '#a855f7', '#ec4899'],
sunset: ['#f43f5e', '#fb923c', '#facc15', '#f59e0b', '#ec4899']
};
function renderPaletteCard(name, isPicker) {
const stripes = palettes[name].map(c => `<span style="background:${c}"></span>`).join('');
return `
<div class="palette-card${(isPicker && name === 'meadow') ? ' 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.butterflies('#butterfly-container-2', {
palette: 'meadow',
density: 18,
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.butterflies('#butterfly-container-2', {
palette: name,
density: 18,
respectMotionPreference: false
});
});
}Demo: Tune the Flight
Adjust the controls and click Apply.
<label>Density</label>
<input type="range" id="opt-density" min="5" max="60" value="18">
<span id="val-density">18</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>Wander</label>
<input type="range" id="opt-wander" min="0.2" max="3" step="0.1" value="1">
<span id="val-wander">1</span>
<label>Rise Speed</label>
<input type="range" id="opt-rise" min="0" max="2" step="0.1" value="0.4">
<span id="val-rise">0.4</span>
<label>Flap Speed</label>
<input type="range" id="opt-flap" min="0.2" max="3" step="0.1" value="1">
<span id="val-flap">1</span>
<div class="butterfly-container mt-4" id="butterfly-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 Flight - from public/showcase/effects/butterflies.
*/
export function mount(root) {
// ── Demo 3: Customisation ────────────────────────────────────────────
const sliders = ['density', 'speed', 'wander', 'rise', 'flap'];
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.butterflies('#butterfly-container-3', {
palette: 'meadow',
density: parseInt($(root).find('#opt-density').val(), 10),
speed: parseFloat($(root).find('#opt-speed').val()),
wander: parseFloat($(root).find('#opt-wander').val()),
riseSpeed: parseFloat($(root).find('#opt-rise').val()),
flapSpeed: parseFloat($(root).find('#opt-flap').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 Release
In burst mode, a single flutter of butterflies is released and the effect winds down naturally - perfect for confirmation moments (form submission, achievement unlocked, sale completed).
<div class="butterfly-container" id="butterfly-container-4">
<div class="butterfly-container-label">
<div style="font-size: 2rem; margin-bottom: 0.5rem;">🌼</div>
<div>Click "Release a flutter" for a one-shot burst</div>
</div>
</div>
<button type="button" class="btn btn-primary" id="burst-4">Release a flutter</button>/**
* Demo: One-Shot Release - from public/showcase/effects/butterflies.
*/
export function mount(root) {
// ── Demo 4: One-shot release ─────────────────────────────────────────
$(root).find('#burst-4').on('click', () => {
Domma.effects.butterflies('#butterfly-container-4', {
palette: 'pastel',
burst: true,
burstCount: 30,
speed: 1.2,
respectMotionPreference: false
});
});
}Domma.effects.butterflies('#card', {
burst: true,
burstCount: 30,
palette: 'pastel',
speed: 1.2
});
Demo: Full-Page Overlay
Pass null as the selector to release butterflies across the whole viewport. They drift 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/butterflies.
*/
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.butterflies(null, {
palette: 'rainbow',
density: 24,
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.butterflies(null, {
palette: 'rainbow',
density: 24,
zIndex: 9999
});
Use Cases
- Success & celebration moments - fire a one-shot
burston form submission, checkout completion, or an achievement unlock. - Ambient hero backdrops - a low-density continuous swarm behind a landing-page headline adds life without distraction.
- Empty-state & onboarding delight - soften a blank dashboard or welcome screen with a gentle flutter.
- Seasonal & spring theming - pair the
meadoworpastelpalette with seasonal campaigns and promotions.