---
name: nikuma-butterflies
description: Add drifting butterflies (light theme) and fireflies (dark theme) inside a box or hero section. Use when the user asks for this effect or component in any framework (vanilla JS, Astro, React, Vue, Svelte, Solid).
---

# Butterflies

Little butterflies drift around a box and flutter their wings. Switch the theme and they become glowing fireflies. This is the same code that flies around the hero of this site.

One dependency-free file. `mountButterflies(element, options)` builds everything inside `element` and **returns a `stop()` function** that removes it and cleans up all listeners. The framework wrappers below only call it in the right lifecycle hook.

## When to use

The user wants: butterflies by day, fireflies by night. Works in plain HTML, Astro, React (incl. Next.js), Vue (incl. Nuxt), Svelte (incl. SvelteKit) and Solid.

## How to add it

1. Save the file(s) under **Source** into the project (for example `src/lib/butterflies/`), keeping their names. They need no installation.
2. Vanilla / Astro / plain HTML: import the function in a `<script type="module">` and call it with the container element. Other frameworks: create the wrapper from **Framework wrappers**, then render it.
3. Give the container a size where the rules below say so, then check it in both light and dark theme.

```js
import { mountButterflies } from './butterflies.js';
const stop = mountButterflies(document.getElementById('stage'), { butterflies: 6, fireflies: 14 });
// stop();  // when the element is removed
```

## Options (all optional)

| Option | Default | What it does |
|---|---|---|
| `butterflies` | `6` | how many butterflies (fewer on small phones) |
| `fireflies` | `14` | how many fireflies |
| `mode` | `'auto'` | 'auto' follows the theme, or force 'butterflies' / 'fireflies' |
| `palette` | `5 pairs` | list of `[wingColor, lightWingColor]` pairs |

## Rules

- The container must have a height (the critters fly inside it).
- Do not mount it twice on the same element; call the returned `stop()` first.
- It reads the theme from `<html data-theme="dark|light">`; without that attribute it follows the system setting.
- Call `stop()` before mounting again on the same element (the framework wrappers do this on unmount).
- Do not edit the engine to change behaviour; use options. Respect `prefers-reduced-motion` (the engine does).

## Source

### `butterflies.js`

```js
// Butterflies (light) / fireflies (dark) drifting around inside a box.
// mountButterflies(el, options) -> stop()   No dependencies, no CSS file (styles are added for you).
//
// options (all optional):
//   butterflies  how many butterflies           default 6
//   fireflies    how many fireflies             default 14
//   mode         'auto' | 'butterflies' | 'fireflies'
//                'auto' = butterflies in the light theme, fireflies in the dark theme (reads <html data-theme="dark"> or the OS setting)
//   palette      list of [wing, wing-light] colour pairs
const PALETTE = [['#FF6B94', '#FFB3C7'], ['#F0953A', '#FFD08A'], ['#4B82E8', '#A9C6FF'], ['#A35BFF', '#D5B5FF'], ['#F2C230', '#FFE58A']];
const rand = (a, b) => a + Math.random() * (b - a);
const pick = (a) => a[Math.floor(Math.random() * a.length)];
const css = (id, t) => { if (!document.getElementById(id)) { const s = document.createElement('style'); s.id = id; s.textContent = t; document.head.append(s); } };

const wings = ([a, b]) => `<svg viewBox="0 0 24 24" aria-hidden="true">
<g class="bfx-wing"><path d="M12 12C8 3 1 3 2 9c.4 3 5 4 10 3z" fill="${a}"/><path d="M12 12C7 13 3 16 5 20c2 2 6-2 7-8z" fill="${b}"/></g>
<g class="bfx-wing"><path d="M12 12c4-9 11-9 10-3-.4 3-5 4-10 3z" fill="${a}"/><path d="M12 12c5 1 9 4 7 8-2 2-6-2-7-8z" fill="${b}"/></g>
<rect x="11.2" y="6.5" width="1.6" height="11.5" rx=".8" fill="#3a2a1a"/></svg>`;

export function mountButterflies(el, o = {}) {
  css('bfx-css', `.bfx-box{position:absolute;inset:0;overflow:hidden;pointer-events:none}
.bfx{position:absolute;left:0;top:0;will-change:transform;width:var(--s,18px);height:var(--s,18px)}
.bfx-r{width:100%;height:100%;filter:drop-shadow(0 2px 2px rgba(0,0,0,.18))}.bfx svg{width:100%;height:100%;overflow:visible}
.bfx-wing{transform-box:view-box;transform-origin:12px 12px;animation:bfx-flap var(--flap,.32s) ease-in-out infinite alternate}
@keyframes bfx-flap{from{transform:scaleX(1)}to{transform:scaleX(.22)}}
.bfx-ff{width:var(--s,4px);height:var(--s,4px)}
.bfx-ff i{position:absolute;inset:0;border-radius:50%;background:#F4FF9A;box-shadow:0 0 6px 2px rgba(222,255,120,.85),0 0 18px 7px rgba(190,255,90,.35);animation:bfx-glow var(--glow,4s) ease-in-out var(--delay,0s) infinite}
@keyframes bfx-glow{0%,100%{opacity:.08;scale:.7}45%,60%{opacity:1;scale:1.15}}`);
  if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
  const box = document.createElement('div'); box.className = 'bfx-box'; box.setAttribute('aria-hidden', 'true'); el.append(box);
  const root = document.documentElement;
  const isDark = () => (root.dataset.theme ? root.dataset.theme === 'dark' : matchMedia('(prefers-color-scheme: dark)').matches);
  const palette = o.palette || PALETTE;
  let list = [], W = 0, H = 0, raf = 0, last = 0, visible = true;
  const spot = () => ({ x: rand(22, Math.max(40, W - 22)), y: rand(22, Math.max(40, H - 22)) });

  function make(fly) {
    const d = document.createElement('div'), c = { el: d, ...spot(), vx: 0, vy: 0, heading: 0, wait: 0, ph: rand(0, 6.28), wf: rand(1.2, 2.4) };
    c.tx = c.x; c.ty = c.y;
    if (!fly) {
      d.className = 'bfx'; d.style.cssText = `--s:${rand(13, 20)}px;--flap:${rand(.24, .4).toFixed(2)}s`;
      d.innerHTML = `<div class="bfx-r">${wings(pick(palette))}</div>`;
      c.rot = d.firstChild; c.speed = rand(28, 52); c.turn = rand(1.6, 2.6); c.amp = rand(10, 22);
    } else {
      d.className = 'bfx bfx-ff'; d.style.cssText = `--s:${rand(3, 5).toFixed(1)}px;--glow:${rand(2.8, 6).toFixed(1)}s;--delay:${rand(-6, 0).toFixed(1)}s`;
      d.innerHTML = '<i></i>'; c.speed = rand(7, 18); c.turn = rand(.6, 1.2); c.amp = rand(4, 10);
    }
    box.append(d); return c;
  }
  function build() {
    list.forEach((c) => c.el.remove());
    const fly = o.mode === 'fireflies' || ((o.mode || 'auto') === 'auto' && isDark());
    const n = Math.round((fly ? o.fireflies ?? 14 : o.butterflies ?? 6) * (W < 420 ? .6 : 1));
    list = Array.from({ length: n }, () => make(fly));
  }
  function frame(t) {
    const dt = Math.min(.05, (t - last) / 1000 || .016); last = t;
    for (const c of list) {
      c.wait -= dt;
      if (c.wait <= 0 || Math.hypot(c.tx - c.x, c.ty - c.y) < 14) { const s = spot(); c.tx = s.x; c.ty = s.y; c.wait = rand(2, 5); }
      const d = Math.hypot(c.tx - c.x, c.ty - c.y) || 1, k = Math.min(1, dt * c.turn);
      c.vx += ((c.tx - c.x) / d * c.speed - c.vx) * k; c.vy += ((c.ty - c.y) / d * c.speed - c.vy) * k;
      c.ph += dt * c.wf;
      const sp = Math.hypot(c.vx, c.vy) || 1, wob = Math.sin(c.ph) * c.amp;      // a little sideways wobble = flutter
      c.x += c.vx * dt + (-c.vy / sp) * wob * dt * 2; c.y += c.vy * dt + (c.vx / sp) * wob * dt * 2;
      c.x = Math.min(W - 14, Math.max(14, c.x)); c.y = Math.min(H - 14, Math.max(14, c.y));
      c.el.style.transform = `translate3d(${c.x.toFixed(1)}px,${c.y.toFixed(1)}px,0)`;
      if (c.rot) {                                                                  // turn smoothly to face where it flies
        const target = Math.atan2(c.vy, c.vx) * 180 / Math.PI + 90, diff = ((((target - c.heading) % 360) + 540) % 360) - 180;
        c.heading += diff * Math.min(1, dt * 4); c.rot.style.rotate = c.heading.toFixed(1) + 'deg';
      }
    }
    raf = requestAnimationFrame(frame);
  }
  const sync = () => {
    const run = !document.hidden && visible && W > 0;
    if (run && !raf) { last = performance.now(); raf = requestAnimationFrame(frame); }
    if (!run && raf) { cancelAnimationFrame(raf); raf = 0; }
  };
  const ro = new ResizeObserver(() => { const first = !W; W = box.clientWidth; H = box.clientHeight; if (first && W) build(); sync(); });
  ro.observe(el);
  const io = new IntersectionObserver(([e]) => { visible = e.isIntersecting; sync(); }); io.observe(el);
  const mo = new MutationObserver(build); mo.observe(root, { attributes: true, attributeFilter: ['data-theme'] });   // swap butterflies <-> fireflies with the theme
  document.addEventListener('visibilitychange', sync);
  return () => { cancelAnimationFrame(raf); ro.disconnect(); io.disconnect(); mo.disconnect(); document.removeEventListener('visibilitychange', sync); box.remove(); };
}
```

## Framework wrappers

### React: `Butterflies.jsx`

```jsx
'use client'; // only needed in Next.js
import { useEffect, useRef } from 'react';
import { mountButterflies } from './butterflies.js';

export default function Butterflies({ options = {}, className, style }) {
  const ref = useRef(null);
  useEffect(() => mountButterflies(ref.current, options), []);   // the engine returns its own cleanup
  return <div ref={ref} className={className} style={style} />;
}
```

### Vue: `Butterflies.vue`

```vue
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { mountButterflies } from './butterflies.js';

const props = defineProps({ options: { type: Object, default: () => ({}) } });
const root = ref(null);
let stop;
onMounted(() => { stop = mountButterflies(root.value, props.options); });
onBeforeUnmount(() => stop && stop());
</script>

<template><div ref="root"></div></template>
```

### Svelte: `Butterflies.svelte`

```svelte
<script>
  import { onMount } from 'svelte';
  import { mountButterflies } from './butterflies.js';

  export let options = {};
  export let style = '';
  let klass = '';
  export { klass as class };
  let root;
  onMount(() => mountButterflies(root, options));   // returning the cleanup from onMount stops it on destroy
</script>

<div bind:this={root} class={klass} {style}></div>
```

### Solid: `Butterflies.jsx`

```jsx
import { onMount, onCleanup } from 'solid-js';
import { mountButterflies } from './butterflies.js';

export default function Butterflies(props) {
  let root;
  onMount(() => onCleanup(mountButterflies(root, props.options || {})));
  return <div ref={root} class={props.class} style={props.style} />;
}
```
