---
title: SparkleWrapper
description: Animates its children with a bounce-enter while Diamond sparkle icons burst outward, fading and scaling to nothing. Use it to celebrate any element on mount.
---

```tsx
'use client';

import { useState } from 'react';
import { SparkleWrapper } from '@/ui/SparkleWrapper';
import { Icon } from '@clubmed/trident-icons';

export function SparkleWrapperDemo() {
  const [key, setKey] = useState(0);

  return (
    <div className="flex flex-col items-center gap-48 p-48">
      <SparkleWrapper className="text-green" count={3} key={key}>
        <Icon name="CheckFilled" className="!text-green" width="75px" />
      </SparkleWrapper>

      {/* <SparkleWrapper key={`b-${key}`}>
        <span className="text-h4 font-bold">🎉 Congratulations!</span>
      </SparkleWrapper> */}

      <button
        type="button"
        onClick={() => setKey((k) => k + 1)}
        className="px-20 py-10 rounded-pill border-2 border-black font-semibold text-b3 hover:bg-black hover:text-white transition-colors cursor-pointer"
      >
        Replay animation
      </button>
    </div>
  );
}
```

## Installation

Using the CLI:

```bash
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/sparkle-wrapper.json
```

Or manually, copy the following code into `/ui/SparkleWrapper.tsx`:

```tsx
'use client';

import { Icon } from '@clubmed/trident-icons';
import type { HTMLAttributes, PropsWithChildren } from 'react';
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import clsx from 'clsx';

function randomBetween(min: number, max: number) {
  return min + Math.random() * (max - min);
}

function generateSparkles(count: number) {
  return Array.from({ length: count }, () => ({
    angle: Math.random() * 360,
    distance: randomBetween(60, 110),
    stagger: Math.round(randomBetween(0, 150)),
    size: Math.round(randomBetween(20, 28)),
  }));
}

function edgeT(cos: number, sin: number, hw: number, hh: number): number {
  if (Math.abs(cos) < 1e-6) return hh / Math.abs(sin);
  if (Math.abs(sin) < 1e-6) return hw / Math.abs(cos);
  return Math.min(hw / Math.abs(cos), hh / Math.abs(sin));
}

export interface SparkleWrapperProps extends HTMLAttributes<HTMLDivElement> {
  count?: number;
  /** Delay in milliseconds before the sparkles start flying. Children still animate in immediately. */
  delay?: number;
}

export function SparkleWrapper({
  children,
  className,
  count = 7,
  delay = 0,
  ...props
}: PropsWithChildren<SparkleWrapperProps>) {
  const childRef = useRef<HTMLDivElement>(null);
  const [halfSize, setHalfSize] = useState<[number, number]>([0, 0]);
  const [sparkles] = useState(() => generateSparkles(Math.round(randomBetween(count, count + 3))));

  const [isSparkling, setIsSparkling] = useState(delay <= 0);

  useEffect(() => {
    if (delay <= 0) return;
    const timeout = setTimeout(() => setIsSparkling(true), delay);
    return () => clearTimeout(timeout);
  }, [delay]);

  useLayoutEffect(() => {
    if (!childRef.current) return;
    const { width, height } = childRef.current.getBoundingClientRect();
    setHalfSize([width / 2, height / 2]);
  }, []);

  return (
    <div {...props} className={clsx('relative inline-flex items-center justify-center', className)}>
      {isSparkling &&
        sparkles.map(({ angle, distance, stagger, size }, i) => {
          const rad = (angle * Math.PI) / 180;
          const cos = Math.cos(rad);
          const sin = Math.sin(rad);
          const [hw, hh] = halfSize;
          const t = hw > 0 || hh > 0 ? edgeT(cos, sin, hw, hh) : 0;

          return (
            <Icon
              key={i}
              name="Diamond"
              aria-hidden="true"
              className="absolute top-1/2 left-1/2 animate-sparkle-fly pointer-events-none"
              style={
                {
                  '--sparkle-start-x': `${Math.round(cos * t)}px`,
                  '--sparkle-start-y': `${Math.round(sin * t)}px`,
                  '--sparkle-end-x': `${Math.round(cos * (t + distance))}px`,
                  '--sparkle-end-y': `${Math.round(sin * (t + distance))}px`,
                  animationDelay: `${stagger}ms`,
                  width: `${size}px`,
                  height: `${size}px`,
                } as React.CSSProperties
              }
            />
          );
        })}
      <div ref={childRef} className="relative z-10 animate-bounce-enter">
        {children}
      </div>
    </div>
  );
}
```

## Usage

```tsx showLineNumbers
import { SparkleWrapper } from '@/ui/SparkleWrapper';

export function Component() {
  return (
    <SparkleWrapper>
      <div className="bg-saffron px-20 py-12 rounded-pill font-bold">New feature</div>
    </SparkleWrapper>
  );
}
```

## How it works

When the component mounts, two things happen simultaneously:

1. **Children** animate in with `bounce-enter` — scaling up from 50%, overshooting slightly, then settling at natural size.
2. **8 Diamond sparkles** burst outward from the center in all directions (every 45°), each scaling down from 100% to 0 while fading out. Delays are staggered between 0–100ms and sizes vary (10–16px) for a natural, organic burst.

Both animations run once and do not repeat.

### Delaying the sparkles

Pass `delay` (in milliseconds) to hold the sparkle burst back while the children still bounce in immediately. Sparkles are not rendered at all until the delay elapses, so nothing is visible beforehand. A good starting point is to let the bounce settle first:

```tsx showLineNumbers
<SparkleWrapper delay={400}>
  <Icon name="CheckFilled" width="75px" />
</SparkleWrapper>
```

## API Reference

### SparkleWrapper

| Prop        | Type                             | Default     | Description                                                                         |
| ----------- | -------------------------------- | ----------- | ----------------------------------------------------------------------------------- |
| `children`  | `ReactNode`                      | —           | The content to animate on mount                                                     |
| `count`     | `number`                         | `7`         | Minimum number of sparkles (renders `count` to `count + 3`)                         |
| `delay`     | `number`                         | `0`         | Milliseconds to wait before the sparkles start flying; children animate immediately |
| `className` | `string`                         | `undefined` | Additional classes applied to the root `div`                                        |
| `...props`  | `HTMLAttributes<HTMLDivElement>` | —           | All standard div attributes are forwarded                                           |

## Notes

- The root element is `inline-flex` so it shrinks to fit its content. Wrap it in a block container if you need full-width layout.
- Animations fire once on mount. To replay them, unmount and remount the component (use a `key` prop). Changing `delay` after the sparkles have started does not replay them.
- Sparkle icons are `aria-hidden="true"` — they are purely decorative and invisible to assistive technology.
- The `bounce-enter` duration is 500ms; sparkles complete in 600ms. Nothing lingers after 700ms (including the longest 100ms delay).
