# Celebration burst

> A brief confetti or sparkle burst that rises from the control that earned it, drawn on a temporary canvas and gone in 1.6 s by default. Pair it with a real text confirmation; it never replaces one.

- ID: `cmp_celebration_burst_01`
- Slug: `celebration-burst-01`
- Version: `1.0.0` (current)
- Status: published
- Published: 2026-09-22
- Updated: 2026-09-22
- Available versions: `1.0.0`
- Kind: control
- Primary category: `visual-effects`
- Detail page: https://pagesugar.com/components/celebration-burst-01?variant=brand
- Preview: https://pagesugar.com/preview/celebration-burst-01

## Variants

| Variant | Label | Default | Artifact digest |
| --- | --- | --- | --- |
| `multicolour` | Multicolour | yes | `sha256-b929d8eebe5c3abdd40cada129665fa0f8aac6c0eabec74b34c34c5db291d6ce` |
| `brand` | Brand (one hue) | no | `sha256-8b3cf79920f6f74a85433a17e02d1bc6d0bfa20e70a120e05a00f86dba37dc7d` |

## Runtime and compatibility

- Runtime: svelte
- Svelte: 5
- SvelteKit required: no (portable Svelte component)
- Tailwind CSS: 4
- SSR: supported
- Requires client-side JavaScript: yes
- Integration level: local-interaction
- Appearance modes: light
- Suggested directory: `src/lib/components/celebration-burst-01`

## Dependencies

No third-party runtime packages.

## Services

No external services required.

## Usage

Wrap the control that completes the action and change trigger after your backend confirms success. The component draws one burst on a temporary canvas and removes it after duration. It does not confirm anything: keep your own success message, because the canvas is hidden from assistive technology and the burst is skipped entirely when the visitor prefers reduced motion. It plays no sound and makes no requests.

Required props: none

```svelte
<!-- Fire only after your own request succeeds, and keep a visible confirmation. -->
<script lang="ts">
	import CelebrationBurst from '$lib/components/celebration-burst-01/CelebrationBurst.svelte';

	let celebrate = $state(0);
	let status = $state('');

	async function placeOrder() {
		const response = await fetch('/api/orders', { method: 'POST' });
		if (!response.ok) {
			status = 'We could not place the order. Try again in a moment.';
			return;
		}
		status = 'Order placed. The receipt is on its way to your inbox.';
		celebrate += 1;
	}
</script>

<CelebrationBurst trigger={celebrate}>
	<button type="button" onclick={placeOrder}>Place order</button>
</CelebrationBurst>
<p role="status" aria-live="polite">{status}</p>
```

Limitations:

- Nothing is announced: the canvas is aria-hidden, so the text confirmation is yours to provide.
- Under prefers-reduced-motion the trigger does nothing; there is no static alternative.
- The canvas is sized when the burst starts. Resizing the window during a burst is not tracked.
- With scope="viewport" the wrapper is a div with display: contents and the canvas is appended to document.body; the wrapper is still a div in the HTML, so place the component in flow content, not inside a paragraph.
- With scope="container" the canvas covers the wrapper's box and copies the first child's border-radius, so the child should fill the wrapper. A wrapper with no size draws nothing.
- duration has a 300 ms floor and no ceiling; non-finite values fall back to 1600. Removal is scheduled by a browser timer, so a suspended tab can be a little late.

## Usage guide

### Using the burst

Copy `CelebrationBurst.svelte` into `src/lib/components/celebration-burst-01/`. Wrap the control that completes the action, keep a counter in state, and add one to it after your request succeeds. Each change to `trigger` plays one burst; the value the component mounts with never does.

#### Pair it with a confirmation

The burst is decoration for a success that is already on screen. The canvas is `aria-hidden`, nothing is announced, and when the visitor has asked for reduced motion the trigger is ignored outright. So the sequence is always: the server confirms, your success state or status message appears (a polite live region, or a component covering the success-state pattern), and then `trigger` changes. Never fire it optimistically, and never let it be the only sign that something worked.

#### Origin and scope

`origin="element"` (the default) starts the burst at the top of the first element inside the wrapper, so confetti rises out of the button rather than through it. `origin="center"` starts from the middle of the scope. `{ x, y }` are fractions of the scope's width and height, which is the way to aim at a number or a headline.

`scope="viewport"` appends a fixed canvas to `document.body`, which keeps it clear of any ancestor with a transform. `scope="container"` makes the wrapper a positioned block and draws inside it, following the first child's `border-radius` so pieces stay inside a rounded card; wrap the card itself, not its contents.

#### Rapid triggers

Triggers that land while a burst is playing join it: one canvas, one burst, removed at the end of the first one's `duration`. A double-submitted form or an effect that runs twice therefore produces one celebration, not two.

#### Colours

The pieces cycle through `--celebration-burst-1` to `--celebration-burst-4`, read from the wrapper at the moment the burst starts, so set them on any ancestor. The `colors` prop replaces the tokens for one instance. Four steps of one hue reads as a brand; four unrelated hues reads as a party. Pick deliberately.

#### Cleanup

The canvas is removed when the burst ends, when a hidden tab's timer fires, or when the component is destroyed, and the animation frame and timer go with it.

## Props

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `trigger` | `number` | no | `0` | Change the value (usually increment it) to fire one burst. The value at mount never fires, and changes during a burst join the running one. |
| `shape` | 'confetti' \| 'sparkles' | no | `'confetti'` | Tumbling paper in an upward cone, or four-point glints that drift outward and shimmer. Read when a burst starts; a change mid-burst applies to the next one. |
| `scope` | 'viewport' \| 'container' | no | `'viewport'` | A fixed canvas over the whole viewport, or one clipped to the wrapper's box and its first child's corners. Changing scope ends a running burst. |
| `origin` | 'element' \| 'center' \| { x: number; y: number } | no | `'element'` | Where the burst starts: the top edge of the wrapped element, the centre of the scope, or fractions of the scope's width and height. Without children, 'element' falls back to the centre. |
| `particleCount` | `number` | no | `64 for confetti, 12 for sparkles` | Pieces per burst, capped at 150. |
| `duration` | `number` | no | `1600` | Milliseconds from the trigger until the canvas is removed, 300 at least. Confetti fades over the last 30 %. |
| `colors` | `string[]` | no | `the four palette tokens` | CSS colours assigned to pieces in turn. A list with at least one non-blank entry replaces the palette tokens; an empty list falls back to them. Without tokens or colours the burst is zinc: neutral by default, like every component here. |
| `children` | `Snippet` | no |  | The control or surface the burst belongs to. It is rendered unchanged and used as the origin. |

## Customization

Set the four palette variables on any ancestor, or pass colors for a one-off list. Shape, origin, scope, count and duration are props; the physics constants sit at the top of fire() in the source.

- Colours: set --celebration-burst-1 to --celebration-burst-4 on an ancestor, or pass colors={\[...\]} to bypass the tokens for one instance. The source's own fallbacks are four zinc steps, so an unstyled burst is monochrome.
- Brand palette: give the four tokens four steps of one hue so the burst reads as yours rather than as a party.
- Origin: leave 'element' so the burst rises from the control; use 'center' for a page-level moment, or { x, y } fractions to aim at a number or a headline.
- Scope: 'container' keeps the burst inside a card. Wrap the card itself, not its contents, so the clip matches its corners.
- Weight: particleCount and duration tune the size of the moment. 150 pieces is the hard cap; 2.6 s reads as a ceremony and 0.9 s as a nod.
- Physics: gravity, spread, speed and decay are named constants inside spawnConfetti and spawnSparkles in the source.
- Layering: the canvas uses z-\[9999\]; lower it in the class list if your own overlays sit above it.

| Token | Public CSS variable |
| --- | --- |
| `color1` | `--celebration-burst-1` |
| `color2` | `--celebration-burst-2` |
| `color3` | `--celebration-burst-3` |
| `color4` | `--celebration-burst-4` |

## Accessibility

- The canvas is aria-hidden and pointer-events: none, so assistive technology never sees it and the page stays clickable while it plays.
- You provide the confirmation: a visible message in a polite live region, or a success state, alongside the trigger. The burst carries no meaning on its own.
- When prefers-reduced-motion is set the trigger is ignored and no canvas is created; if it is turned on during a burst, the burst ends. Your confirmation must stand alone.
- Pieces are small and each crosses the screen once. Confetti tumbles at no more than about 2.3 turns a second and sparkles shimmer at 1.5 Hz with a 15 % depth, so no region flashes in the WCAG 2.3.1 sense; check your own colours against the general flash threshold if you go far outside the defaults.
- Focus is never moved, and the wrapped control keeps its own semantics; in viewport scope the wrapper is a plain div with display: contents, in container scope a positioned block.

Known limitations:

- No static alternative is drawn under reduced motion; add your own if the moment needs a visual mark for every visitor.

## License

- Declared source: MIT
- Default license approval is pending. See https://pagesugar.com/docs/license.

## Source

- Palette: Brand (one hue) (`brand`)
- Entry: `CelebrationBurst.svelte`
- Suggested directory: `src/lib/components/celebration-burst-01`
- Files: 1
- Artifact digest: `sha256-8b3cf79920f6f74a85433a17e02d1bc6d0bfa20e70a120e05a00f86dba37dc7d`

Paths below are relative to the suggested directory. Copy the files as they are;
they import nothing from this site.

#### `CelebrationBurst.svelte`

Role: entry · 13517 bytes · SHA-256 `375bd0ad25c89fbab44ca0c887e7e53ef331e32de93802d69599d89825abba4a`

```svelte
<script module lang="ts">
	export type BurstShape = 'confetti' | 'sparkles';
	export type BurstScope = 'viewport' | 'container';
	/**
	 * Where the burst starts. `element` is the top of the wrapped element (the control that
	 * earned it); `center` is the middle of the scope; `{ x, y }` are fractions of the scope's
	 * width and height, 0 to 1.
	 */
	export type BurstOrigin = 'element' | 'center' | { x: number; y: number };
</script>

<script lang="ts">
	import type { Snippet } from 'svelte';
	import { untrack } from 'svelte';

	interface Props {
		/** Change the value (usually increment it) to fire one burst. The initial value never fires. */
		trigger?: number;
		shape?: BurstShape;
		scope?: BurstScope;
		origin?: BurstOrigin;
		/** Pieces per burst, capped at 150. Defaults to 64 for confetti and 12 for sparkles. */
		particleCount?: number;
		/** Milliseconds from the trigger to the canvas being removed. */
		duration?: number;
		/** CSS colours for the pieces. Defaults to the four palette tokens. */
		colors?: string[];
		/** The element the burst belongs to. With `scope="container"` it is also the clip. */
		children?: Snippet;
	}

	let {
		trigger = 0,
		shape = 'confetti',
		scope = 'viewport',
		origin = 'element',
		particleCount,
		duration = 1600,
		colors,
		children
	}: Props = $props();

	const MAX_PARTICLES = 150;
	/** The neutral fallbacks: the burst is monochrome until a palette variant or the consumer colours it. */
	const FALLBACK_COLORS = ['#09090b', '#3f3f46', '#71717a', '#a1a1aa'];
	/** Physics runs in 60 fps ticks; frames are scaled to that so speed is independent of the refresh rate. */
	const TICK_MS = 1000 / 60;

	interface Particle {
		x: number;
		y: number;
		vx: number;
		vy: number;
		size: number;
		color: string;
		piece: 'rect' | 'disc' | 'star';
		angle: number;
		spin: number;
		wobble: number;
		wobbleSpeed: number;
		decay: number;
		/** Sparkles only: the fraction of the burst at which this one appears and disappears. */
		start: number;
		end: number;
		phase: number;
	}

	interface Burst {
		canvas: HTMLCanvasElement;
		frame: number;
		timer: ReturnType<typeof setTimeout>;
		/** Stops watching the motion preference. */
		unwatch: () => void;
	}

	/** The area the canvas covers, in viewport coordinates. */
	interface Box {
		left: number;
		top: number;
		width: number;
		height: number;
	}

	let wrapper = $state<HTMLDivElement>();
	let burst: Burst | null = null;
	// The value at mount is a baseline, not a trigger.
	let seen = untrack(() => trigger);

	const rand = (min: number, max: number) => min + Math.random() * (max - min);
	const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));

	function resolvedCount(): number {
		const fallback = shape === 'sparkles' ? 12 : 64;
		const wanted = Number.isFinite(particleCount) ? Math.round(particleCount as number) : fallback;
		return clamp(wanted, 1, MAX_PARTICLES);
	}

	function resolvedColors(el: HTMLElement): string[] {
		const own = (colors ?? []).map((c) => c.trim()).filter(Boolean);
		if (own.length) return own;
		const style = getComputedStyle(el);
		const fromTokens = ['--_color-1', '--_color-2', '--_color-3', '--_color-4']
			.map((name) => style.getPropertyValue(name).trim())
			.filter(Boolean);
		return fromTokens.length ? fromTokens : FALLBACK_COLORS;
	}

	/** The element the burst belongs to: the first rendered child, never a previous canvas. */
	function subject(el: HTMLElement): Element | null {
		for (const child of el.children) {
			if (child instanceof HTMLCanvasElement) continue;
			return child;
		}
		return null;
	}

	function resolveOrigin(el: HTMLElement, box: Box): { x: number; y: number } {
		if (typeof origin === 'object' && origin !== null) {
			const fraction = (value: number) => (Number.isFinite(value) ? clamp(value, 0, 1) : 0.5);
			return { x: fraction(origin.x) * box.width, y: fraction(origin.y) * box.height };
		}
		if (origin === 'element') {
			const rect = subject(el)?.getBoundingClientRect();
			if (rect && (rect.width > 0 || rect.height > 0)) {
				// The burst rises out of the control's top edge rather than through it.
				return {
					x: rect.left + rect.width / 2 - box.left,
					y: rect.top - 2 - box.top
				};
			}
		}
		return { x: box.width / 2, y: box.height / 2 };
	}

	function spawnConfetti(
		count: number,
		at: { x: number; y: number },
		box: Box,
		palette: string[]
	): Particle[] {
		// Reach scales with the space in both axes: a card gets a short hop, a phone a narrow
		// fan, a desktop viewport a real arc. Heavy drag makes the rise quick and the fall a flutter.
		const speed = clamp(Math.min(box.width * 0.055, box.height * 0.055), 10, 24);
		// Most pieces leave along the fan's shoulders, 18–54° off vertical and balanced left and
		// right, so they land beside the control rather than back on top of it. One in five goes
		// nearer straight up to keep the crown.
		return Array.from({ length: count }, (_, i) => {
			const off = (Math.random() < 0.2 ? rand(0, 18) : rand(18, 54)) * (Math.PI / 180);
			const direction = -Math.PI / 2 + (Math.random() < 0.5 ? off : -off);
			const velocity = speed * rand(0.6, 1);
			return {
				x: at.x,
				y: at.y,
				vx: Math.cos(direction) * velocity,
				vy: Math.sin(direction) * velocity,
				size: rand(0.7, 1.3),
				color: palette[i % palette.length],
				piece: Math.random() < 0.72 ? 'rect' : 'disc',
				angle: rand(0, Math.PI * 2),
				spin: rand(-0.12, 0.12),
				wobble: rand(0, Math.PI * 2),
				wobbleSpeed: rand(0.06, 0.12),
				decay: rand(0.89, 0.92),
				start: 0,
				end: 1,
				phase: 0
			};
		});
	}

	function spawnSparkles(
		count: number,
		at: { x: number; y: number },
		box: Box,
		palette: string[]
	): Particle[] {
		// Glints sit around the control: each appears at its own spot a short way out, turns a
		// quarter as it blooms, and drifts only a few pixels. Three size tiers (full spans) give depth.
		const reach = clamp(Math.min(box.width, box.height) * 0.09, 24, 56);
		const tiers = [7, 11, 17];
		return Array.from({ length: count }, (_, i) => {
			const direction = rand(0, Math.PI * 2);
			const distance = reach * rand(0.35, 1);
			const drift = rand(0.15, 0.5);
			const start = rand(0, 0.5);
			return {
				x: at.x + Math.cos(direction) * distance,
				y: at.y + Math.sin(direction) * distance * 0.7,
				vx: Math.cos(direction) * drift,
				vy: Math.sin(direction) * drift,
				size: tiers[i % tiers.length] * rand(0.85, 1.15),
				color: palette[i % palette.length],
				piece: 'star',
				angle: rand(-0.4, 0.4),
				spin: rand(0.012, 0.024),
				wobble: 0,
				wobbleSpeed: 0,
				decay: 0.97,
				start,
				end: Math.min(1, start + rand(0.3, 0.5)),
				phase: rand(0, Math.PI * 2)
			};
		});
	}

	function drawStar(ctx: CanvasRenderingContext2D, r: number) {
		// A four-point glint: the curves pull toward the centre, so the points stay sharp.
		ctx.beginPath();
		ctx.moveTo(0, -r);
		ctx.quadraticCurveTo(0, 0, r, 0);
		ctx.quadraticCurveTo(0, 0, 0, r);
		ctx.quadraticCurveTo(0, 0, -r, 0);
		ctx.quadraticCurveTo(0, 0, 0, -r);
		ctx.closePath();
		ctx.fill();
	}

	function drawConfetti(
		ctx: CanvasRenderingContext2D,
		p: Particle,
		progress: number,
		at: { x: number; y: number }
	) {
		// Full strength for 70 % of the burst, then a linear fade so nothing snaps away.
		let alpha = progress < 0.7 ? 1 : (1 - progress) / 0.3;
		// A piece falling back through the control's own column thins out, so the label it
		// earned stays readable; the outer pieces keep their weight.
		if (p.vy > 0 && p.y > at.y - 40 && Math.abs(p.x - at.x) < 64) alpha *= 0.35;
		if (alpha <= 0) return;
		ctx.save();
		ctx.globalAlpha = alpha;
		ctx.fillStyle = p.color;
		ctx.translate(p.x, p.y);
		ctx.rotate(p.angle);
		if (p.piece === 'rect') {
			// The tilt is a scale through zero, which is what makes paper look like it tumbles.
			ctx.scale(1, Math.cos(p.wobble));
			ctx.fillRect(-6.5 * p.size, -2.5 * p.size, 13 * p.size, 5 * p.size);
		} else {
			ctx.scale(1, 0.75 + 0.25 * Math.cos(p.wobble));
			ctx.beginPath();
			ctx.arc(0, 0, 3.2 * p.size, 0, Math.PI * 2);
			ctx.fill();
		}
		ctx.restore();
	}

	function drawSparkle(
		ctx: CanvasRenderingContext2D,
		p: Particle,
		progress: number,
		elapsedMs: number
	) {
		if (progress < p.start || progress > p.end) return;
		const u = (progress - p.start) / (p.end - p.start);
		// Scale in fast, hold with a slow shimmer, scale out; alpha follows the same envelope.
		const envelope = u < 0.2 ? u / 0.2 : u > 0.65 ? (1 - u) / 0.35 : 1;
		// The shimmer is fixed at 1.5 Hz of real time, whatever the duration, and only 15 % deep.
		const shimmer = 0.85 + 0.15 * Math.sin((elapsedMs / 1000) * Math.PI * 3 + p.phase);
		const scale = envelope * shimmer;
		if (scale <= 0) return;
		ctx.save();
		ctx.globalAlpha = Math.min(1, envelope + 0.1);
		ctx.fillStyle = p.color;
		ctx.translate(p.x, p.y);
		ctx.rotate(p.angle);
		drawStar(ctx, (p.size / 2) * scale);
		ctx.restore();
	}

	function finish() {
		if (!burst) return;
		cancelAnimationFrame(burst.frame);
		clearTimeout(burst.timer);
		burst.unwatch();
		burst.canvas.remove();
		burst = null;
	}

	function fire() {
		const el = wrapper;
		if (!el || typeof window === 'undefined') return;
		// One celebration at a time: triggers that land during a burst join it.
		if (burst) return;
		const motion = window.matchMedia?.('(prefers-reduced-motion: reduce)');
		if (motion?.matches) return;

		const contained = scope === 'container';
		const box: Box = contained
			? el.getBoundingClientRect()
			: { left: 0, top: 0, width: window.innerWidth, height: window.innerHeight };
		if (box.width < 1 || box.height < 1) return;

		const canvas = document.createElement('canvas');
		// Two complete class lists, so the page stays clickable and the canvas sits above everything.
		canvas.className = contained
			? 'celebration-burst__canvas pointer-events-none absolute inset-0 z-[9999] block h-full w-full'
			: 'celebration-burst__canvas pointer-events-none fixed inset-0 z-[9999] block h-full w-full';
		canvas.setAttribute('aria-hidden', 'true');
		if (contained) {
			// Follow the wrapped surface's corners so nothing lands outside a rounded card.
			const radius = subject(el) ? getComputedStyle(subject(el) as Element).borderRadius : '';
			if (radius) canvas.style.borderRadius = radius;
		}
		const dpr = clamp(window.devicePixelRatio || 1, 1, 3);
		canvas.width = Math.round(box.width * dpr);
		canvas.height = Math.round(box.height * dpr);
		const ctx = canvas.getContext('2d');
		if (!ctx) return;
		ctx.scale(dpr, dpr);

		const palette = resolvedColors(el);
		const at = resolveOrigin(el, box);
		const count = resolvedCount();
		// Everything about this burst is settled now; a prop that changes mid-flight waits for the next.
		const sparkles = shape === 'sparkles';
		const particles = sparkles
			? spawnSparkles(count, at, box, palette)
			: spawnConfetti(count, at, box, palette);
		const total = Number.isFinite(duration) ? Math.max(300, duration) : 1600;
		// Light gravity against heavy drag: pieces flutter down at a paper's pace, slower in a card.
		const gravity = sparkles ? 0 : 0.34 * clamp(box.height / 900, 0.55, 1);
		const started = performance.now();
		let previous = started;

		(contained ? el : document.body).appendChild(canvas);

		const step = (now: number) => {
			const elapsed = now - started;
			const progress = elapsed / total;
			if (progress >= 1) {
				finish();
				return;
			}
			// Cap the step so a stalled tab does not teleport every piece off screen.
			const ticks = Math.min((now - previous) / TICK_MS, 3);
			previous = now;
			ctx.clearRect(0, 0, box.width, box.height);
			for (const p of particles) {
				const drag = Math.pow(p.decay, ticks);
				p.vx *= drag;
				p.vy = p.vy * drag + gravity * ticks;
				p.x += p.vx * ticks + (p.piece === 'rect' ? Math.sin(p.wobble) * 0.35 * ticks : 0);
				p.y += p.vy * ticks;
				p.angle += p.spin * ticks;
				p.wobble += p.wobbleSpeed * ticks;
				if (p.y > box.height + 24 || p.x < -24 || p.x > box.width + 24) continue;
				if (p.piece === 'star') drawSparkle(ctx, p, progress, elapsed);
				else drawConfetti(ctx, p, progress, at);
			}
			if (burst) burst.frame = requestAnimationFrame(step);
		};

		// A visitor who turns reduced motion on mid-burst gets the quiet they asked for.
		const onMotionChange = () => {
			if (motion?.matches) finish();
		};
		motion?.addEventListener?.('change', onMotionChange);

		burst = {
			canvas,
			frame: requestAnimationFrame(step),
			// requestAnimationFrame pauses in a hidden tab; the canvas still goes at the duration.
			timer: setTimeout(finish, total),
			unwatch: () => motion?.removeEventListener?.('change', onMotionChange)
		};
	}

	$effect(() => {
		// Object.is, so a NaN baseline never reads as a change.
		if (Object.is(trigger, seen)) return;
		seen = trigger;
		untrack(fire);
	});

	$effect(() => {
		// A burst belongs to the scope it started in; changing scope ends it rather than orphaning it.
		void scope;
		untrack(finish);
	});

	$effect(() => finish);
</script>

<div
	bind:this={wrapper}
	class={['celebration-burst', scope === 'container' ? 'relative' : 'contents']}
>
	{#if children}
		{@render children()}
	{/if}
</div>

<style>
	.celebration-burst {
		--_color-1: var(--celebration-burst-1, #312e81);
		--_color-2: var(--celebration-burst-2, #4338ca);
		--_color-3: var(--celebration-burst-3, #6366f1);
		--_color-4: var(--celebration-burst-4, #a5b4fc);
	}
</style>
```

## Artifacts

### Brand (one hue) (`brand`)

- Artifact digest: `sha256-8b3cf79920f6f74a85433a17e02d1bc6d0bfa20e70a120e05a00f86dba37dc7d`
- Entry: `CelebrationBurst.svelte`
- Receipt: https://pagesugar.com/artifacts/cmp_celebration_burst_01/1.0.0/brand/sha256-8b3cf79920f6f74a85433a17e02d1bc6d0bfa20e70a120e05a00f86dba37dc7d/manifest.json
- Bundle: https://pagesugar.com/artifacts/cmp_celebration_burst_01/1.0.0/brand/sha256-8b3cf79920f6f74a85433a17e02d1bc6d0bfa20e70a120e05a00f86dba37dc7d/bundle.zip (8546 bytes, sha256 `73324d5b6563d78b10e9d05522956f5bfb8f73ca28de27062dd3a41e4577c9dd`)

Files:

- `CelebrationBurst.svelte` (entry, 13517 bytes): https://pagesugar.com/artifacts/cmp_celebration_burst_01/1.0.0/brand/sha256-8b3cf79920f6f74a85433a17e02d1bc6d0bfa20e70a120e05a00f86dba37dc7d/source/CelebrationBurst.svelte
