Skip to content

Confetti

Confetti animations are best used to delight your users when something special happens.

Installation

Install the following dependency:

bash
pnpm add canvas-confetti

Copy and paste the following code into your project:

ts
import type { Options as ConfettiOptions } from "canvas-confetti";
import type { InjectionKey } from "vue";

export interface ConfettiApi {
  fire: (options?: ConfettiOptions) => Promise<void>;
}

export const confettiApiKey: InjectionKey<ConfettiApi> = Symbol("confetti-api");
vue
<script setup lang="ts">
import type {
  GlobalOptions as ConfettiGlobalOptions,
  CreateTypes as ConfettiInstance,
  Options as ConfettiOptions,
} from "canvas-confetti";
import { onBeforeUnmount, onMounted, provide, ref } from "vue";
import { cn } from "@/lib/utils";
import { confettiApiKey } from "./context";

interface ConfettiProps {
  class?: string;
  options?: ConfettiOptions;
  globalOptions?: ConfettiGlobalOptions;
  manualstart?: boolean;
}

const props = withDefaults(defineProps<ConfettiProps>(), {
  globalOptions: () => ({ resize: true, useWorker: true }),
  manualstart: false,
});

defineOptions({ inheritAttrs: false });

const canvasRef = ref<HTMLCanvasElement | null>(null);
const instance = ref<ConfettiInstance | null>(null);

async function fire(opts: ConfettiOptions = {}) {
  try {
    await instance.value?.({ ...props.options, ...opts });
  } catch (error) {
    console.error("Confetti error:", error);
  }
}

const api = { fire };

provide(confettiApiKey, api);
defineExpose(api);

onMounted(async () => {
  if (!canvasRef.value) return;
  const confetti = (await import("canvas-confetti")).default;
  if (!canvasRef.value) return;
  instance.value = confetti.create(canvasRef.value, {
    ...props.globalOptions,
    resize: true,
  });

  if (!props.manualstart) {
    await fire();
  }
});

onBeforeUnmount(() => {
  if (instance.value) {
    instance.value.reset();
    instance.value = null;
  }
});
</script>

<template>
  <canvas ref="canvasRef" :class="cn(props.class)" v-bind="$attrs" />
  <slot />
</template>
vue
<script setup lang="ts">
import type {
  GlobalOptions as ConfettiGlobalOptions,
  Options as ConfettiOptions,
} from "canvas-confetti";
import { cn } from "@/lib/utils";

type ConfettiButtonOptions = ConfettiOptions &
  ConfettiGlobalOptions & { canvas?: HTMLCanvasElement };

interface ConfettiButtonProps {
  class?: string;
  options?: ConfettiButtonOptions;
}

const props = defineProps<ConfettiButtonProps>();

async function handleClick(event: MouseEvent) {
  try {
    const target = event.currentTarget as HTMLButtonElement;
    const rect = target.getBoundingClientRect();
    const x = rect.left + rect.width / 2;
    const y = rect.top + rect.height / 2;
    const confetti = (await import("canvas-confetti")).default;
    await confetti({
      ...props.options,
      origin: {
        x: x / window.innerWidth,
        y: y / window.innerHeight,
      },
    });
  } catch (error) {
    console.error("Confetti button error:", error);
  }
}
</script>

<template>
  <button
    :class="
      cn(
        'inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md bg-neutral-900 px-4 py-2 text-sm font-medium text-white shadow transition-colors hover:bg-neutral-900/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 disabled:pointer-events-none disabled:opacity-50 dark:bg-white dark:text-black dark:hover:bg-white/90',
        props.class,
      )
    "
    type="button"
    @click="handleClick"
  >
    <slot />
  </button>
</template>

Usage

vue
<script setup lang="ts">
import { ref } from "vue";
import Confetti from "@/components/spark-ui/confetti/confetti.vue";

const confettiRef = ref<InstanceType<typeof Confetti> | null>(null);
</script>

<template>
  <Confetti ref="confettiRef" @mouseenter="confettiRef?.fire({})" />
</template>

Examples

Basic

Random Direction

Fireworks

Side Cannons

Stars

Custom Shapes

Emoji

Props

Confetti

PropTypeDefaultDescription
classstringundefinedAdditional classes applied to the canvas element.
optionsConfettiOptionsundefinedDefault confetti options merged into every fire call.
globalOptionsConfettiGlobalOptions{ resize: true, useWorker: true }Global options passed to confetti.create for the canvas.
manualstartbooleanfalseWhen true, confetti does not fire automatically on mount.

The component also forwards any extra attributes (e.g. style, event listeners) to the underlying <canvas> element.

Confetti Options

PropTypeDefaultDescription
particleCountInteger50The number of confetti particles to launch
angleNumber90The angle in degrees at which to launch confetti
spreadNumber45The spread in degrees of the confetti
startVelocityNumber45The initial velocity of the confetti
decayNumber0.9The rate at which confetti slows down
gravityNumber1The gravity applied to confetti particles
driftNumber0The horizontal drift applied to particles
flatBooleanfalseWhether confetti particles are flat
ticksNumber200The number of frames confetti lasts
originObject{ x: 0.5, y: 0.5 }The origin point of the confetti
colorsArray of Strings['#26ccff', '#a25afd', '#ff5e7e', '#88ff5a', '#fcff42', '#ffa62d', '#ff36ff']Array of color strings in HEX format
shapesArray of Strings['square', 'circle', 'star']Array of shapes for the confetti
zIndexInteger100The z-index of the confetti
disableForReducedMotionBooleanfalseDisables confetti for users who prefer no motion
useWorkerBooleantrueUse Web Worker for better performance
resizeBooleantrueWhether to resize the canvas
canvasHTMLCanvasElement or nullnullCustom canvas element to draw confetti
scalarNumber1Scaling factor for confetti size

Confetti Exposed Methods

MethodSignatureDescription
fire(options?: ConfettiOptions) => Promise<void>Fires the confetti with optional overrides.

ConfettiButton

PropTypeDefaultDescription
classstringundefinedAdditional classes for the button.
optionsConfettiButtonOptionsundefinedOptions for the confetti burst.

ConfettiButton Slots

SlotDescription
defaultContent to render inside the button.

Credits

Released under the MIT License.