# konfeti

Zero-dependency, strictly typed, highly customizable canvas confetti — with first-class **emoji, text, image
and spritesheet** particles, composable physics and a typed plugin API.

**English** · [Türkçe](https://konfeti.mbattaloglu.com/docs/tr.md)

**npm**

```sh
npm install konfeti
```

**pnpm**

```sh
pnpm add konfeti
```

**yarn**

```sh
yarn add konfeti
```

```ts
import { Konfeti } from "konfeti";

Konfeti.fire(); // classic confetti pop
```

## API at a glance

```ts
import {
  Konfeti, // the ready-made, shared instance: draws on a fullscreen overlay canvas
  KonfetiFactory, // builds extra instances on your own canvas (Web Worker: createWorker in konfeti/worker)
  KonfetiPresets, // ready-to-fire looks: SNOW, FIREWORKS, STARS …
  extendPreset, // a preset plus your own settings
  type FireOptions, // the type of every fire() setting, for autocomplete
} from "konfeti";

// ── the shared instance: nothing to set up ──
Konfeti.fire(); // one classic confetti pop, upward from just below the screen center
Konfeti.fire({ particleCount: 120, spread: 90, origin: { x: 0.5, y: 0.3 } }); // your own settings
Konfeti.fire(KonfetiPresets.SNOW); // a preset, as is
Konfeti.fire(extendPreset(KonfetiPresets.SNOW, { particleCount: 60 })); // a preset, tweaked

const burst = Konfeti.fire(); // every fire() returns a handle for that burst…
burst.pause(); // …freeze just this burst
burst.resume(); // …continue it
await burst; // …or wait until its last particle is gone

const trail = Konfeti.emit({ rate: 40, follow: "pointer" }); // a stream that follows the pointer…
trail.stop(); // …until you stop it

Konfeti.onClick(button, { particleCount: 30 }); // fire from the click position on every click
Konfeti.pause(); // freeze everything (e.g. while an ad is not visible)
Konfeti.resume(); // continue everything
Konfeti.reset(); // remove every particle at once

// ── your own instance: own canvas, own defaults, own particle limit ──
const stage = KonfetiFactory.create(canvas, { maxParticles: 800 });
stage.fire(); // same API as Konfeti, drawn on `canvas`
stage.destroy(); // stop, clear and remove its listeners when you are done

// ── typed options: every setting autocompletes and is checked ──
const options: FireOptions = { particleCount: 80, paper: { colors: ["#d6ff3f", "#ffffff"] } };
Konfeti.fire(options);
```

## Contents

- [API at a glance](#api-at-a-glance)
- [Basics](#basics)
- [The paper particle](#the-paper-particle)
- [Shapes](#shapes)
- [Physics](#physics)
- [Emission](#emission)
- [Continuous emitter](#continuous-emitter)
- [Formations](#formations)
- [Presets](#presets)
- [Hooks](#hooks)
- [Instances & canvases](#instances--canvases)
- [Worker rendering](#worker-rendering)
- [Playable ads & webviews](#playable-ads--webviews)
- [Custom shapes & physics](#custom-shapes--physics)
- [Bundle size & `konfeti/lite`](#bundle-size--konfetilite)
- [Units & ranges](#units--ranges)
- [AI assistants](#ai-assistants)

## Basics

```ts
import { Konfeti } from "konfeti";

const burst = Konfeti.fire({
  particleCount: 120,
  angle: 60, // 90 = up, 0 = right
  spread: 55,
  origin: { x: 0, y: 0.7 }, // normalized canvas position
});

burst.pause();
burst.resume();
await burst; // resolves when every particle is gone
```

`origin` can also be an **element** (particles start from its center) or a **click event**:

```ts
button.addEventListener("click", (event) => Konfeti.fire({ origin: event }));
Konfeti.fire({ origin: document.querySelector("#buy")! });
```

Every numeric option accepts a fixed value or a random **range**: `10`, `[8, 14]` or `{ min: 8, max: 14 }`.

Every handle reports its random **seed**. Fire the same options with it to get the same burst again: the same
particles, colors and launch values. On an instance created with `fixedTimestep: true` the replay is exact,
motion included, on any display and at any frame rate:

```ts
const seed = Konfeti.fire({ particleCount: 80, spread: 90 }).getSeed();
setTimeout(() => Konfeti.fire({ particleCount: 80, spread: 90, seed }), 1200); // the same burst again
```

```ts
const stage = KonfetiFactory.create(canvas, { fixedTimestep: true }); // 1/60 s steps: exact replays
```

## The paper particle

The default confetti piece is fully configurable through `paper`:

```ts
Konfeti.fire({
  paper: {
    form: ["rect", "circle", { value: "strip", weight: 2 }], // weighted mix of silhouettes
    width: [6, 10],
    height: [12, 18],
    cornerRadius: 2, // or { tl: 6, br: 6 }
    skew: [-15, 15],
    colors: ["#ff0a54", "#ffd000", { color: "#00c2ff", weight: 3 }],
    backColor: "auto", // darkened front color while flipped
    stroke: { color: "white", width: 0.5 },
    gradient: { colors: ["gold", "orange"], angle: 45 },
    flip: { axis: "both", frequency: [0.5, 1.5] },
    wobble: { amplitude: [2, 8] },
    fadeOut: { start: 0.6, easing: "easeInQuad" },
    scaleOverLife: { to: 0.4 },
    shine: 0.5,
    trail: { length: 12 }, // a fading streak behind each piece
  },
});
```

Color themes: `KonfetiPalettes.PASTEL`, `GOLD`, `NEON`, `RAINBOW`, `WINTER`, `AUTUMN`, `OCEAN`, `CANDY`, `FOREST`,
`MONOCHROME` and `CLASSIC` are ready-made color lists — `paper: { colors: KonfetiPalettes.GOLD }`.

Forms: `rect`, `square`, `circle`, `strip`, `leaf`. Other style keys: `scale`, `colorMode`, `backShade`,
`colorOverLife`, `opacity`, `fadeIn`, `rotation`, `rotationSpeed`, `tilt`, `shadow`, `blendMode`, `trail` (it works on every
shape, e.g. `{ type: "star", trail: true }`).
Every option is documented in the type definitions — hover it in your editor.

## Shapes

Mix any shapes with weights. Style keys set in `paper` are the base for every shape; each entry can override
them.

```ts
Konfeti.fire({
  paper: { colors: ["#ff0a54", "#ffd000"] },
  shapes: [
    { type: "paper", weight: 4 },
    { type: "star", points: [5, 6], shine: 0.6 },
    { type: "heart", colors: "#ff4d6d" },
    { type: "emoji", emoji: ["🎉", "🥳", { value: "✨", weight: 3 }], size: [22, 32] },
    { type: "text", text: ["YAY", "WOW"], fontWeight: 900 },
    { type: "image", src: "/coin.png", size: [20, 30] },
    { type: "spritesheet", src: "/coin-spin.png", frames: { cols: 8, rows: 1 }, fps: 14 },
  ],
});
```

| Type          | Specific options                                                   |
| ------------- | ------------------------------------------------------------------ |
| `paper`       | paper geometry (see above)                                         |
| `star`        | `size`, `points`, `innerRatio`                                     |
| `triangle`    | `size`                                                             |
| `polygon`     | `size`, `sides`                                                    |
| `heart`       | `size`                                                             |
| `ribbon`      | `length`, `thickness`, `waves`                                     |
| `path`        | `path` (SVG `d` or `Path2D`), `viewBox`, `size`                    |
| `emoji`       | `emoji`, `size` (font size), `fontFamily`                          |
| `text`        | `text`, `size` (font size), `fontFamily`, `fontWeight`             |
| `image`       | `src` (URL or any canvas image source), `size` (width), `tint`     |
| `spritesheet` | `src`, `frames`, `fps`, `loop`, `randomStartFrame`, `size`, `tint` |

Emoji and text are rasterized once and cached. Preload URL images with `loadImage(url)`. An `image` or `spritesheet`
`src` that starts with `<svg` is used as inline SVG markup (give it a `width` and `height`; not supported in worker
mode).

`tint` paints images and spritesheets in the particle colors (`colors`, or the `paper` colors), so one white
artwork gives a whole palette. `true` multiplies the color in and keeps the shading; `"fill"` draws flat
silhouettes. Each color is painted once, so drawing costs the same as without a tint.

```ts
Konfeti.fire({
  shapes: [
    {
      type: "image",
      src: "/coin.png",
      size: [20, 30],
      tint: "fill",
      colors: ["#26ccff", "#ff5e7e"],
    },
  ],
});
```

A sheet packed by TexturePacker, Aseprite or Free Texture Packer comes with an atlas JSON: `framesFromAtlas`
turns it into `frames`, in atlas order, optionally only the frames whose name starts with a prefix.

```ts
import { Konfeti, framesFromAtlas } from "konfeti";
import atlas from "./coins.json";

Konfeti.fire({
  shapes: [{ type: "spritesheet", src: "/coins.png", frames: framesFromAtlas(atlas, "coin_spin") }],
});
```

## Physics

```ts
Konfeti.fire({
  physics: {
    gravity: 700, // px/s², negative floats upward
    drag: 3.5, // 1/s
    wind: [-40, 40], // px/s²
    terminalVelocity: 900, // px/s
    swirl: { strength: [80, 200], frequency: [0.3, 0.8] },
    floor: { y: 1, bounce: 0.35, friction: 0.4 }, // particles bounce and rest
    attract: { target: "pointer", strength: 900 }, // pulled toward the pointer; negative pushes away
  },
});
```

`attract` targets an element (measured every frame), `"pointer"` or a point like `{ x: 0.5, y: 0.2 }`; `radius`
limits its reach and `falloff: "linear"` fades it toward the edge.

## Emission

```ts
Konfeti.fire({ emission: { mode: "burst" } }); // default: everything at once
Konfeti.fire({ particleCount: 200, emission: { mode: "stream", duration: 3000 } }); // fountain
Konfeti.fire({
  particleCount: 50,
  emission: { mode: "interval", every: 350, times: 8 },
  origin: { x: [0.2, 0.8] },
});
```

Interval shots sample the origin once per shot — ranged origins produce fireworks at different spots.

`delay` starts a burst later (in milliseconds; paused time does not count). Give the entries of a list different
delays to choreograph them:

```ts
Konfeti.fire([
  { origin: { x: 0, y: 0.8 }, angle: 60 },
  { origin: { x: 1, y: 0.8 }, angle: 120, delay: 250 },
  { origin: { x: 0.5, y: 0.5 }, spread: 360, startVelocity: [500, 1000], delay: 700 },
]);
```

## Continuous emitter

`emit()` streams particles until you stop it — a fountain, a trail behind the pointer, sparks from an
element. It takes every `fire()` setting except `particleCount` / `origin` / `emission`, plus `rate`
(particles per second) and `follow`:

```ts
const trail = Konfeti.emit({
  rate: 60, // particles per second
  follow: "pointer", // or an element (measured every emission), or a point like { x: 0.5, y: 1 }
  spread: 360,
  startVelocity: [50, 150],
  lifetime: 800,
  shapes: [{ type: "star", size: [6, 10] }],
});

setTimeout(() => trail.stop(), 3000); // stop emitting; particles already out finish their lives
await trail; // resolves once the last one is gone
```

`trail.moveTo(target)` switches what it follows, `clear()` removes everything at once, and `pause()` /
`resume()` freeze it. Worker instances (`konfeti/worker`) emit too; the pointer or element is tracked on the
main thread.

## Formations

Particles first form a text or an image, hold it, then burst apart: a "You win!" end card, a launch
announcement, a logo reveal. Every shape can take part, and the rest of the burst options apply as usual once
the shape breaks up.

```ts
Konfeti.fire({
  origin: { x: 0.5, y: 0.45 }, // the center of the shape
  formation: {
    text: "TEBRİKLER",
    font: "900 110px sans-serif",
    mode: "assemble", // fly in from beyond the edges; "appear" shows the shape at once
    assemble: 900, // ms until the shape is complete
    hold: 1200, // ms before it bursts apart
    spacing: 8, // px between particles: smaller is denser and uses more particles
  },
  shapes: [{ type: "paper", weight: 3 }, { type: "star" }],
});
```

```ts
// an image: its opaque pixels become the shape, painted in the image's own colors
Konfeti.fire({
  formation: {
    image:
      '<svg width="220" height="200" viewBox="0 0 24 22"><path fill="#ff3d6e" d="M12 21s-9-6.1-9-12a5 5 0 0 1 9-3 5 5 0 0 1 9 3c0 5.9-9 12-9 12z"/></svg>',
    mode: "appear",
    hold: 1500,
  },
  startVelocity: [200, 500], // how fast the pieces fly outward when the shape breaks up
});
```

- The particle count follows from `spacing`; an explicit `particleCount` only caps it. When `maxParticles`
  caps it, the whole shape thins out evenly instead of losing a part.
- `fit` (default `0.9`) scales the whole formation (shape, spacing and particle size) down until it fits
  the canvas, so a phone shows a smaller copy of the same picture.
- `startVelocity` is the burst-apart speed, `[300, 700]` px/s unless you set it. The particles' `lifetime`
  starts when they are released, so the shape stays fully visible until then.
- An image can be a URL, inline SVG markup or any drawable image; the burst waits until it has loaded.
  `imageColors: false` keeps the shapes' own colors. A cross-origin image needs CORS headers.
- Worker instances form shapes too, with the fonts the worker can see; inline SVG is not supported there.
  Formations need a single burst (`emission: { mode: "burst" }`, the default) and are not available for
  `emit()`.

## Presets

```ts
import { Konfeti, KonfetiPresets, extendPreset } from "konfeti";

Konfeti.fire(KonfetiPresets.FIREWORKS);
Konfeti.fire(extendPreset(KonfetiPresets.SNOW, { emission: { mode: "stream", duration: 10000 } })); // your settings win
```

`BASIC`, `REALISTIC`, `CANNON`, `SIDE_SHOTS`, `SCHOOL_PRIDE`, `FIREWORKS`, `SNOW`, `STARS`, `EMOJI_RAIN`,
`HEART_BURST`, plus presets that show off newer options: `SHOOTING_STARS` (trails), `MAGNET` (attractor),
`FORCE_FIELD` (repulsor), `GOLDEN` (gold palette), `SPARKLER` and `FIREFLIES` (additive glow), `JACKPOT` (floor),
`CONGRATS`, `SUCCESS` and `LEVEL_UP` (text formations; `LEVEL_UP` is choreographed with `delay`) and `LOGO_REVEAL`
(image formation). Presets are plain, read-only options (`SIDE_SHOTS` and `SCHOOL_PRIDE` are two bursts). `extendPreset`
merges your settings into every burst without changing the preset; `paper` and `physics` merge key by key.

## Hooks

```ts
Konfeti.fire({
  onStart: () => {},
  onParticleSpawn: (p) => {},
  onParticleUpdate: (p, dt) => {}, // hot path, may mutate p.x / p.vx …
  onParticleDeath: (p) => {},
  onComplete: () => {},
});
```

## Instances & canvases

`Konfeti` is a shared fullscreen instance. Use `KonfetiFactory.create()` for your own canvas, separate defaults or a separate particle budget:

```ts
import { KonfetiFactory } from "konfeti";

const stage = KonfetiFactory.create(document.querySelector("canvas"), {
  maxParticles: 800,
  disableForReducedMotion: true,
  defaults: { paper: { colors: ["gold", "white"] } },
});

stage.fire({ particleCount: 80 });
const off = stage.onClick(document.querySelector("#like")!, { particleCount: 30 });
off(); // stop listening
stage.reset();
stage.destroy(); // also removes its click listeners
```

`onClick` takes a third argument: `trigger: "pointerdown"` fires as soon as a finger or button goes down (it
keeps working when the page cancels touch events), and `onFire` receives each click burst's handle:

```ts
Konfeti.onClick(
  button,
  { particleCount: 30 },
  {
    trigger: "pointerdown",
    onFire: (burst) => burst.then(() => console.log("done")),
  },
);
```

`pause()` freezes every burst on its current frame and `resume()` continues without counting the paused time
(hidden tabs pause on their own). Both exist on `Konfeti` and on every instance.

The first instance on a page logs a one-line banner with the version and renderer to the console. Call
`disableBanner()` before firing to keep the console silent (`konfeti/lite` never prints it).

## Worker rendering

`createWorker()` from `konfeti/worker` moves simulation and drawing into a Web Worker through an `OffscreenCanvas`, so
large bursts keep animating while the main thread is busy (heavy React renders, long tasks, scrolling):

```ts
import { createWorker } from "konfeti/worker"; // its own entry: other bundles carry no worker code

const stage = createWorker(document.querySelector("canvas"), { maxParticles: 3000 });

await stage.fire({
  particleCount: 1500,
  shapes: [{ type: "paper" }, { type: "emoji", emoji: "🎉" }],
});
stage.isWorker(); // false when the browser cannot render offscreen — it then runs on the main thread
stage.destroy(); // terminates the worker
```

Pass `null` (or nothing) for the canvas to get a fullscreen overlay, like `Konfeti`.

Options travel to the worker by `postMessage`, so they must be structured-cloneable. The types
(`WorkerFireOptions`) enforce this:

| Works in a worker                                      | Main thread only                                     |
| ------------------------------------------------------ | ---------------------------------------------------- |
| every paper, style, physics and emission option        | hooks (`onStart`, `onParticleSpawn`, `onComplete` …) |
| built-in shapes, emoji and text                        | custom `defineShape` / `definePhysics` modules       |
| `image` / `spritesheet` with a URL or an `ImageBitmap` | `HTMLImageElement`, `Path2D` and other DOM sources   |
| `origin` as a point, an element or a click event       | —                                                    |

Element and click origins are measured on the main thread before sending; the pointer or element that an
emitter follows or an attractor pulls toward is tracked there too and sent over while the burst runs. Use
`await` on the handle instead of `onComplete`.

The worker script itself (~19.1 kB brotli) is loaded only when the first worker instance is created. With a
bundler it is inlined as a `blob:` URL; the `<script>` build loads `konfeti.worker.js` from its own folder. For a
strict Content-Security-Policy without `worker-src blob:`, host `konfeti/konfeti.worker.js` yourself:

```ts
createWorker(canvas, { workerUrl: "/vendor/konfeti.worker.js" });
```

If the script cannot load, every burst rejects with a clear error instead of hanging.

Hooks cannot run inside a worker, so a worker instance counts for you: `stage.getStats()` returns
`{ live, spawned, died, completed }` (updated a few times per second).

## Playable ads & webviews

konfeti works in single-file playable ads: it has no dependencies, makes no network requests of its own, uses
no storage and no `eval`. `Konfeti.fire()` adds about 65 kB minified (20 kB brotli); ad networks usually count
uncompressed size. Keep these in mind:

- **Images:** pass a `data:` URI or an image / canvas your engine already loaded — a URL like `/coin.png` is a
  network request.
- **Web fonts:** text and emoji are drawn once and cached; a glyph drawn before its `@font-face` finished
  loading is redrawn automatically when the font arrives.
- **Emoji** look different on every platform (and old Android may lack new ones); use `image` shapes when the
  look must match.
- **Taps:** if your engine cancels touch events, `click` never fires on phones — use
  `onClick(target, options, { trigger: "pointerdown" })` or fire from your own input handler with
  `origin: { x, y }` (0–1) or the pointer event.
- **Visibility:** call `Konfeti.pause()` / `Konfeti.resume()` from MRAID `viewableChange`.
- **Low-end phones:** create the stage with `KonfetiFactory.create(null, { adaptiveQuality: true })`. On a slow
  device it renders at CSS resolution first, then drops shadows, shine and trails, then spawns fewer particles,
  and steps back up when frames are smooth again; `getQualityLevel()` tells the current level.
- **Worker rendering:** avoid it in ads — some webviews block `blob:` workers, and a single-file build that
  inlines dynamic imports carries the 62 kB worker script once you import `konfeti/worker`.
- **Console:** call `disableBanner()` in production builds.
- **Targets:** the build is ES2022 (Chrome 85+ / iOS 14.5+); let your bundler lower it for older webviews.

## Custom shapes & physics

Register your own shapes and physics modules — declare their options once and they are fully typed
everywhere.

```ts
import { Konfeti, defineShape, definePhysics } from "konfeti";

declare module "konfeti" {
  interface ShapeRegistry {
    diamond: { sharpness?: number };
  }
  interface PhysicsRegistry {
    magnet: { x: number; y: number; strength: number };
  }
}

// path-based: gradients, stroke, shine and flip work automatically
defineShape("diamond", {
  viewBox: [24, 24],
  path: ({ sharpness = 0.5 }) => `M12 0L${24 - sharpness * 12} 12L12 24L${sharpness * 12} 12Z`,
});

definePhysics("magnet", {
  defaults: { x: 0.5, y: 0.5, strength: 600 },
  apply: (p, dt, world, { x, y, strength }) => {
    p.vx += (x * world.width - p.x) * strength * dt * 0.001;
    p.vy += (y * world.height - p.y) * strength * dt * 0.001;
  },
});

Konfeti.fire({
  shapes: [{ type: "diamond", sharpness: 0.8 }],
  physics: { magnet: { strength: 900 } },
});
```

Draw-based shapes (`defineShape(name, { draw(ctx, particle, options) { … } })`) get full canvas control
with the transform already applied.

## Bundle size & `konfeti/lite`

| Usage                                                | Size (min + brotli) |
| ---------------------------------------------------- | ------------------- |
| `Konfeti` from `konfeti`                             | ~19.8 kB            |
| everything from `konfeti`                            | ~23.9 kB            |
| `Konfeti` from `konfeti/lite`                        | ~13.9 kB            |
| `createWorker` from `konfeti/worker`                 | ~21.9 kB            |
| worker script (loaded on the first `createWorker()`) | ~19.1 kB            |

`konfeti` registers every built-in shape for you. `konfeti/lite` starts with **paper only** — register just the
shapes you use and your bundler drops the rest:

```ts
import { Konfeti, registerShapes, starShape, emojiShape } from "konfeti/lite";

registerShapes(starShape, emojiShape);
Konfeti.fire({ shapes: [{ type: "star" }, { type: "emoji", emoji: "🎉" }] });
```

Presets live in the full entry only (they use several shapes). Formations are opt-in in lite too: call
`enableFormations()` once.

## Units & ranges

| Alias                      | Meaning                         |
| -------------------------- | ------------------------------- |
| `Pixels`                   | CSS pixels (before DPR scaling) |
| `Degrees`                  | 0 = right, 90 = up              |
| `Milliseconds`             | time                            |
| `Ratio`                    | 0–1 (clamped)                   |
| `Hertz`                    | cycles per second               |
| `PixelsPerSecond(Squared)` | speed / acceleration            |

Invalid options throw a `TypeError` that names the option (`"paper.width" must be a finite number`).

## AI assistants

konfeti ships an [Agent Skill](https://agentskills.io) in the package, `node_modules/konfeti/skills/konfeti`. It
teaches coding agents (Claude Code, Codex, Cursor, GitHub Copilot …) the entries, the core API, recipes, every
preset and shape, and the common pitfalls, and it always matches the installed version. Copy or link the folder
into your agent's skills folder, e.g. for Claude Code:

```sh
mkdir -p .claude/skills && cp -r node_modules/konfeti/skills/konfeti .claude/skills/
```

For chat assistants, the site serves the docs as Markdown:

- [`llms.txt`](https://konfeti.mbattaloglu.com/llms.txt): an index of the Markdown docs, for an assistant that
  can read links
- [`llms-full.txt`](https://konfeti.mbattaloglu.com/llms-full.txt): this guide and the whole API reference in one
  file, to paste or upload
- [`docs/index.md`](https://konfeti.mbattaloglu.com/docs/index.md): this guide alone

The guide's sidebar opens ChatGPT or Claude with a prompt that points at `llms.txt`, and the playground's Share &
Export tab copies a prompt with your current settings as JSON; paste the JSON an assistant returns into Options JSON
and load it into the controls.

## License

[MIT](https://github.com/mbattaloglu/konfeti/blob/main/LICENSE)
