npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zakkster/lite-confetti

v1.1.0

Published

Deterministic confetti engine with OKLCH colors, 5 shapes, reduced-motion support, and timeline composability.

Readme

@zakkster/lite-confetti

npm version npm bundle size npm downloads npm total downloads TypeScript License: MIT

Deterministic confetti engine with OKLCH colors, 5 shapes, and reduced-motion support.

The confetti library that canvas-confetti wishes it was.

→ Live Interactive Playground

Why lite-confetti?

| Feature | lite-confetti | canvas-confetti | react-confetti | party.js | |---|---|---|---|---| | Deterministic (seeded) | Yes | No | No | No | | OKLCH colors | Yes | No | No | No | | Reduced motion | Yes (auto) | No | No | No | | Shapes | 5 (rect, circle, star, triangle, emoji) | 2 | 2 | 3 | | Spray mode | Yes | No | No | No | | Shared ticker | Yes | Own RAF | Own RAF | Own RAF | | SoA flat arrays | Yes | No | No | No | | Timeline composable | Yes | No | No | No | | Zero-GC hot path | Yes | No | No | No | | ResizeObserver | Yes | window.resize | No | No | | Bundle size | < 4KB | ~6KB | ~5KB | ~8KB |

Installation

npm install @zakkster/lite-confetti

Quick Start

One-Liner (Fire and Forget)

import { confetti } from '@zakkster/lite-confetti';

// Creates overlay canvas, fires burst, cleans up automatically
confetti();

Full Control

import { createConfetti } from '@zakkster/lite-confetti';

const c = createConfetti(overlayCanvas, { seed: 42 });

c.burst({ count: 80, spread: 1.2, shape: 'star' });
c.burst({ x: 200, y: 100, shape: 'emoji', emoji: '🎊', count: 30 });

// Later
c.seed(42);  // reset for deterministic replay
c.destroy();

Full Options Reference

Burst Options

Every parameter is optional. Sensible defaults produce a beautiful upward confetti burst.

| Option | Type | Default | Description | |---|---|---|---| | x | number | canvas center | Burst origin X position (CSS pixels) | | y | number | canvas height × 0.33 | Burst origin Y position (CSS pixels) | | count | number | 80 | Number of particles to spawn | | spread | number | 1.2 | Emission cone width in radians (π = half-circle) | | speed | number | 400 | Initial particle speed center (px/s) | | speedVariance | number | 200 | Speed randomness range. Actual speed: speed ± speedVariance | | gravity | number | 600 | Downward acceleration in px/s². Higher = falls faster. | | drag | number | 0.98 | Per-frame velocity retention (0–1). 0.98 = 2% speed loss per frame. | | sizeMin | number | 5 | Minimum particle width in CSS pixels | | sizeMax | number | 12 | Maximum particle width in CSS pixels | | lifeMin | number | 1.5 | Minimum particle lifetime in seconds | | lifeMax | number | 3.0 | Maximum particle lifetime in seconds | | shape | string | 'rect' | Particle shape: 'rect', 'circle', 'star', 'triangle', 'emoji' | | emoji | string | '🎉' | Emoji character (only used when shape is 'emoji') | | colors | Array | 7 OKLCH defaults | Array of OKLCH objects { l, c, h } or CSS strings | | angle | number | -Math.PI / 2 | Center angle of emission cone in radians. -π/2 = upward. | | onComplete | Function | — | Called when all burst particles have died |

Spray Options

Spray accepts all burst options plus:

| Option | Type | Default | Description | |---|---|---|---| | duration | number | 1000 | Spray duration in milliseconds | | rate | number | 5 | Particles spawned per frame |

createConfetti Options

| Option | Type | Default | Description | |---|---|---|---| | seed | number | Date.now() | RNG seed for deterministic output | | maxParticles | number | 500 | Pool size (ring buffer — overwrites oldest when full) | | respectReducedMotion | boolean | true | Honor prefers-reduced-motion: reduce |


Particle Physics Pipeline

Every frame, each alive particle runs through this pipeline:

1. GRAVITY     vy += gravity × dt        (downward acceleration)
2. DRAG        vx *= drag, vy *= drag    (air resistance)
3. POSITION    x += vx × dt, y += vy × dt
4. SPIN        rotation += spinVelocity × dt
5. TILT        tiltPhase += tiltSpeed × dt
6. OPACITY     fade to 0 in last 30% of life
7. RENDER      translate → rotate → wobble-scale → draw shape

Rotation & 3D Tumbling

Each particle has two rotational properties:

Spin — continuous rotation around the particle's center. Angular velocity is randomized at spawn: (rng.next() - 0.5) * 10 radians/second. This produces particles spinning between -5 and +5 rad/s — some clockwise, some counterclockwise, all at different speeds.

Tilt — a wobble phase that drives a cosine-based X-scale oscillation: wobbleScale = 0.5 + |cos(tiltPhase)| × 0.5. This makes particles appear to tumble in 3D — they visually "flip" as the cosine oscillates, creating the illusion of a thin piece of paper turning in space. Tilt speed is randomized between 1 and 5 rad/s per particle.

The combination of spin rotation + tilt wobble produces the realistic confetti tumbling you see in the real world.

Canvas Sizing

lite-confetti uses ResizeObserver (not polling) to track canvas dimensions. The observer watches the canvas's parent element, RAF-deduped to prevent double-fire. clientWidth / clientHeight are never read in the hot loop — only cached cw / ch variables are used during rendering. This prevents layout thrashing at 60fps.


Shapes

| Shape | Description | |---|---| | 'rect' | Classic confetti rectangle (default). Height varies 40–100% of width for natural variation. | | 'circle' | Round confetti dots | | 'star' | 5-pointed star with 40% inner radius | | 'triangle' | Equilateral triangle piece | | 'emoji' | Any emoji character — set via emoji option (e.g. '🌟', '🎊', '❤️') |


Recipes

import { confetti } from '@zakkster/lite-confetti';

submitBtn.addEventListener('click', () => {
    confetti({
        count: 100,
        spread: 1.5,
        colors: [
            { l: 0.7, c: 0.25, h: 130 }, // green
            { l: 0.8, c: 0.2, h: 60 },   // gold
        ],
    });
});
const c = createConfetti(canvas);
c.spray({
    shape: 'emoji',
    emoji: '🌟',
    duration: 2000,
    rate: 4,
    gravity: 300,
    speed: 100,
});
const c = createConfetti(canvas);
c.spray({
    shape: 'circle',
    rate: 3,
    duration: 5000,
    gravity: 80,
    drag: 0.995,
    speed: 50,
    speedVariance: 30,
    sizeMin: 2,
    sizeMax: 5,
    spread: Math.PI,
    angle: Math.PI / 2,
    colors: [{ l: 0.95, c: 0.01, h: 220 }],
});
confetti({
    x: 0,
    y: window.innerHeight,
    angle: -Math.PI / 4,
    spread: 0.4,
    speed: 800,
    gravity: 400,
    count: 60,
    shape: 'star',
});
import { createTimeline } from '@zakkster/lite-timeline';
import { confetti } from '@zakkster/lite-confetti';
import { easeOut } from '@zakkster/lite-lerp';

const tl = createTimeline();

tl.add({ duration: 400, ease: easeOut, onUpdate: t => {
    modal.style.opacity = t;
}})
.add({ duration: 0, onComplete: () => confetti({ y: 200, shape: 'star' }) })
.play();
const c = createConfetti(canvas, { seed: 42 });
c.burst({ count: 50 });

c.seed(42);
c.burst({ count: 50 }); // exact same output
import { generateTheme } from '@zakkster/lite-theme-gen';
import { confetti } from '@zakkster/lite-confetti';

const theme = generateTheme({ l: 0.6, c: 0.25, h: 280 });

confetti({
    colors: [theme.accent, theme['accent-300'], theme['accent-700']],
    shape: 'circle',
    count: 60,
});

Reduced Motion

lite-confetti automatically detects prefers-reduced-motion: reduce. When active:

  • Particles appear instantly at their spread positions (no flight animation)
  • Hold for 1.5 seconds so users see the celebration
  • Fade out gracefully via CSS opacity transition
  • onComplete still fires

Zero developer effort required. Just call confetti() and it works for everyone.


API

confetti(options?) — Fire and forget

Creates a temporary overlay canvas, fires a burst, cleans up automatically when all particles die.

createConfetti(canvas, options?) — Full control

| Method | Description | |---|---| | .burst(options?) | Classic burst. See full options table above. | | .spray(options?) | Continuous stream. See spray options above. | | .clear() | Kill all particles immediately | | .count | Number of alive particles (getter) | | .seed(n) | Reset RNG for deterministic replay | | .destroy() | Clean up everything. Disconnects ResizeObserver. Idempotent. |


Changelog

v1.1.0

Performance: Zero-GC OKLCH rendering

Moved toCssOklch() color conversion out of the render loop entirely. Colors are now pre-parsed to CSS strings once per burst() / spray() call, before any particles spawn. The render loop reads pre-computed string references with zero allocation.

Before (v1.0.0 — inside render loop, runs every frame for every particle):

ctx.fillStyle = typeof c === 'string' ? c : toCssOklch(c);  // 30,000 strings/sec

After (v1.1.0 — inside burst/spray, runs once per call):

const parsedColors = colors.map(c => typeof c === 'string' ? c : toCssOklch(c));
// render loop:
ctx.fillStyle = colorsArr[i];  // pure reference, zero allocation

Stability: Identity matrix reset on canvas resize

Enforced strict setTransform(1,0,0,1,0,0) identity reset before applying DPR scaling in updateSize(). Prevents potential cumulative scaling bugs when the canvas resizes multiple times during its lifecycle.

ctx.setTransform(1, 0, 0, 1, 0, 0);  // 1. Reset to identity
ctx.scale(dpr, dpr);                   // 2. Apply exact DPR

Stability: RAF-debounced ResizeObserver

Added requestAnimationFrame batching to the ResizeObserver callback. No matter how many times the observer fires during a CSS Grid/Flex reflow, updateSize() executes at most once per frame — preventing layout thrashing.

License

MIT