---
title: Advanced Toast
description: A feature-rich toast notification component with expandable details, progress indicator, and customizable accent colors
---

```tsx
'use client';

import { AdvancedToast } from '@/ui/AdvancedToast';
import { type ToasterProps, Toaster, toast, useToaster } from 'react-hot-toast';
import { getHotToastConfig } from '@/ui/AdvancedToast.config';
import { Colors } from '@/ui/helpers/colors/colors';

export function AdvancedToastDemo() {
  const { handlers } = useToaster();

  const showToast = () => {
    toast.custom(
      (t) => (
        <AdvancedToast
          title="Upload Complete"
          body="Your file has been successfully uploaded"
          icon="CheckFilled"
          isVisible={t.visible}
          labels={{ close: 'Close', details: 'Details' }}
          onClose={() => toast.dismiss(t.id)}
          accentColor={Colors.green}
          toastId={t.id}
          updateHeight={handlers.updateHeight}
        />
      ),
      { duration: Infinity },
    );
  };

  return (
    <>
      <Toaster {...(getHotToastConfig() as ToasterProps)} />
      <button onClick={showToast}>Show Toast</button>
    </>
  );
}
```

## Installation

Using the CLI:

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

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

```tsx
'use client';

import clsx from 'clsx';
import {
  useEffect,
  useId,
  useRef,
  useState,
  type ComponentPropsWithoutRef,
  type FunctionComponent,
} from 'react';

import { Icon, type IconicNames } from '@clubmed/trident-icons';
import { ElasticHeight } from './ElasticHeight';
import { Colors, getBgColor, getBorderColor, getTextColor } from './helpers/colors/colors';
import { useCountdown } from './hooks/useCountdown';
import useResizeObserver from './hooks/useResizeObserver';

interface Props extends ComponentPropsWithoutRef<'div'> {
  title: string;
  body: string;
  icon: IconicNames;
  isVisible: boolean;
  labels: {
    close: string;
    details: string;
  };
  onClose?: () => void;
  children?: React.ReactNode;
  accentColor?: Colors;
  moreInfoLabel?: string;
  moreInfoHref?: string;
  duration?: false | number;
  toastId: string;
  updateHeight: (id: string, height: number) => void;
}

export const AdvancedToast: FunctionComponent<Props> = ({
  className,
  title,
  body,
  icon,
  isVisible,
  labels,
  children,
  accentColor = Colors.orange,
  duration = 10000,
  onClose,
  toastId,
  updateHeight,
  ...attrs
}) => {
  const id = useId();
  const [isDetailsExpanded, setIsDetailsExpanded] = useState(false);
  const [isPaused, setIsPaused] = useState(false);
  const toastRef = useRef<HTMLDivElement>(null!);

  const { progress } = useCountdown({ duration, isPaused, isVisible });
  useEffect(() => {
    if (progress === 100) {
      onClose?.();
    }
  }, [progress, onClose]);

  const handleMouseDown = () => setIsPaused(true);
  const handleMouseUp = () => setIsPaused(false);
  const handleMouseLeave = () => setIsPaused(false);

  useResizeObserver<HTMLDivElement>({
    ref: toastRef,
    onResize: () => {
      if (toastRef.current) {
        updateHeight(toastId, toastRef.current.offsetHeight);
      }
    },
  });

  return (
    <div
      ref={toastRef}
      {...attrs}
      className={clsx(
        isVisible ? 'animate-slide-right-enter' : 'animate-slide-right-exit opacity-0',
        'relative isolate flex gap-x-20 rounded-16 bg-white p-20 text-black transition-opacity duration-300 border-1 overflow-hidden',
        getBorderColor(accentColor),
        className,
      )}
      role="note"
      onMouseDown={handleMouseDown}
      onMouseUp={handleMouseUp}
      onMouseLeave={handleMouseLeave}
    >
      <span className={clsx('w-30 h-30 pt-4', getTextColor(accentColor))}>
        <Icon name={icon} width="30px" className={clsx('flex-shrink-0')} />
      </span>
      <div className="flex flex-1 flex-col gap-12 pe-24 text-b4">
        <div>
          <p className=" font-bold" id={id}>
            {title}
          </p>
          <p>{body}</p>
        </div>

        {children && (
          <div>
            <button
              onClick={() => setIsDetailsExpanded(!isDetailsExpanded)}
              aria-expanded={isDetailsExpanded}
              className="flex w-fit items-center gap-8 text-b5 font-normal outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2"
              type="button"
            >
              <span>{labels.details}</span>
              <Icon
                name="ArrowDefaultDown"
                width="24px"
                style={{ rotate: isDetailsExpanded ? '180deg' : '0deg' }}
                className="transition-all duration-300"
              />
            </button>
            <ElasticHeight isExpanded={isDetailsExpanded}>
              <div className="pt-4">{children}</div>
            </ElasticHeight>
          </div>
        )}
      </div>

      {onClose && (
        <button
          aria-describedby={id}
          aria-label={labels.close}
          className="absolute end-0 top-0 mt-20 me-13 text-black outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2"
          onClick={onClose}
          type="button"
        >
          <Icon name="CrossDefault" width="24px" />
        </button>
      )}

      {duration !== false && (
        <div className="absolute bottom-0 left-0 right-0 h-4 overflow-hidden rounded-b-16">
          <div
            className={getBgColor(accentColor)}
            style={{
              width: '100%',
              height: '100%',
              transform: `scaleX(${(100 - progress) / 100})`,
              transformOrigin: 'left',
              transition: 'transform 16ms linear',
            }}
          />
        </div>
      )}
    </div>
  );
};
```

## Usage

```tsx showLineNumbers
import { AdvancedToast } from '@/ui/AdvancedToast';
import { toast } from 'react-hot-toast';

export function Component() {
  const showToast = () => {
    toast.custom((t) => (
      <AdvancedToast
        title="Upload Complete"
        body="Your file has been successfully uploaded"
        icon="CheckDefault"
        isVisible={t.visible}
        labels={{ close: 'Close', details: 'Details' }}
        onClose={() => toast.dismiss(t.id)}
        accentColor="orange"
      />
    ));
  };

  return <button onClick={showToast}>Show Toast</button>;
}
```

## API Reference

### AdvancedToast

| Prop            | Type                  | Default               | Description                                                                                       |
| --------------- | --------------------- | --------------------- | ------------------------------------------------------------------------------------------------- |
| `title`         | `string`              | required              | The main heading text of the toast                                                                |
| `body`          | `string`              | required              | The descriptive text content of the toast                                                         |
| `icon`          | `IconicNames`         | required              | Icon name from the Trident Icons library                                                          |
| `isVisible`     | `boolean`             | required              | Controls the visibility state and animation of the toast                                          |
| `labels`        | `{ close, details }`  | required              | Accessibility labels for close button and details toggle                                          |
| `onClose`       | `() => void`          | `undefined`           | Dismiss the toast (provided by react-hot-toast)                                                   |
| `children`      | `ReactNode`           | `undefined`           | Optional expandable details content                                                               |
| `accentColor`   | `Colors`              | `Colors.orange`       | Color theme for the border and progress indicator                                                 |
| `duration`      | `false \| number`     | `10000` (10 seconds)  | Auto-dismiss duration in milliseconds, or `false` to disable auto-dismissal                       |
| `toastId`       | `string`              | `undefined`           | Unique identifier for the toast (provided by `react-hot-toast`)                                   |
| `updateHeight`  | `() => void`          | `undefined`           | Callback function to update the height of the toast, get it from `react-hot-toast`'s useToaster   |

## Examples

### Basic Toast

```tsx
'use client';

import { AdvancedToast } from '@/ui/AdvancedToast';
import { type ToasterProps, Toaster, toast, useToaster } from 'react-hot-toast';
import { getHotToastConfig } from '@/ui/AdvancedToast.config';
import { Colors } from '@/ui/helpers/colors/colors';

export function AdvancedToastDemo() {
  const { handlers } = useToaster();

  const showToast = () => {
    toast.custom(
      (t) => (
        <AdvancedToast
          title="Upload Complete"
          body="Your file has been successfully uploaded"
          icon="CheckFilled"
          isVisible={t.visible}
          labels={{ close: 'Close', details: 'Details' }}
          onClose={() => toast.dismiss(t.id)}
          accentColor={Colors.green}
          toastId={t.id}
          updateHeight={handlers.updateHeight}
        />
      ),
      { duration: Infinity },
    );
  };

  return (
    <>
      <Toaster {...(getHotToastConfig() as ToasterProps)} />
      <button onClick={showToast}>Show Toast</button>
    </>
  );
}
```

### With Details

```tsx
'use client';

import { AdvancedToast } from '@/ui/AdvancedToast';
import { type ToasterProps, Toaster, toast, useToaster } from 'react-hot-toast';
import { getHotToastConfig } from '@/ui/AdvancedToast.config';
import { Colors } from '@/ui/helpers/colors/colors';

export function AdvancedToastWithDetailsDemo() {
  const { handlers } = useToaster();

  const showToast = () => {
    toast.custom(
      (t) => (
        <AdvancedToast
          title="Upload Complete"
          body="Your file has been successfully uploaded"
          icon="CheckFilled"
          isVisible={t.visible}
          labels={{ close: 'Close', details: 'Details' }}
          onClose={() => toast.dismiss(t.id)}
          accentColor={Colors.green}
          toastId={t.id}
          updateHeight={handlers.updateHeight}
        >
          <div className="text-b4">
            <p>File: document.pdf</p>
            <p>Size: 2.5 MB</p>
            <p>Uploaded at: {new Date().toLocaleTimeString()}</p>
          </div>
        </AdvancedToast>
      ),
      { duration: Infinity },
    );
  };

  return (
    <>
      <Toaster {...(getHotToastConfig() as ToasterProps)} />
      <button onClick={showToast}>Show Toast</button>
    </>
  );
}
```

### Green Accent (Success)

```tsx
'use client';

import { AdvancedToast } from '@/ui/AdvancedToast';
import { type ToasterProps, Toaster, toast, useToaster } from 'react-hot-toast';
import { getHotToastConfig } from '@/ui/AdvancedToast.config';
import { Colors } from '@/ui/helpers/colors/colors';

export function AdvancedToastGreenDemo() {
  const { handlers } = useToaster();

  const showToast = () => {
    toast.custom(
      (t) => (
        <AdvancedToast
          title="Success!"
          body="Your changes have been saved successfully"
          icon="CheckFilled"
          isVisible={t.visible}
          labels={{ close: 'Close', details: 'Details' }}
          onClose={() => toast.dismiss(t.id)}
          accentColor={Colors.green}
          toastId={t.id}
          updateHeight={handlers.updateHeight}
        />
      ),
      { duration: Infinity },
    );
  };

  return (
    <>
      <Toaster {...(getHotToastConfig() as ToasterProps)} />
      <button onClick={showToast}>Show Toast</button>
    </>
  );
}
```

### Red Accent (Error)

```tsx
'use client';

import { AdvancedToast } from '@/ui/AdvancedToast';
import { type ToasterProps, Toaster, toast, useToaster } from 'react-hot-toast';
import { getHotToastConfig } from '@/ui/AdvancedToast.config';
import { Colors } from '@/ui/helpers/colors/colors';

export function AdvancedToastRedDemo() {
  const { handlers } = useToaster();

  const showToast = () => {
    toast.custom(
      (t) => (
        <AdvancedToast
          title="Error"
          body="Something went wrong. Please try again."
          icon="Error"
          isVisible={t.visible}
          labels={{ close: 'Close', details: 'Details' }}
          onClose={() => toast.dismiss(t.id)}
          accentColor={Colors.red}
          toastId={t.id}
          updateHeight={handlers.updateHeight}
        />
      ),
      { duration: Infinity },
    );
  };

  return (
    <>
      <Toaster {...(getHotToastConfig() as ToasterProps)} />
      <button onClick={showToast}>Show Toast</button>
    </>
  );
}
```

### No Auto-Dismiss

```tsx
'use client';

import { AdvancedToast } from '@/ui/AdvancedToast';
import { type ToasterProps, Toaster, toast, useToaster } from 'react-hot-toast';
import { getHotToastConfig } from '@/ui/AdvancedToast.config';
import { Colors } from '@/ui/helpers/colors/colors';

export function AdvancedToastNoDismissDemo() {
  const { handlers } = useToaster();

  const showToast = () => {
    toast.custom(
      (t) => (
        <AdvancedToast
          title="Important Notice"
          body="This notification will not auto-dismiss"
          icon="Trident"
          isVisible={t.visible}
          labels={{ close: 'Close', details: 'Details' }}
          onClose={() => toast.dismiss(t.id)}
          accentColor={Colors.darkGrey}
          duration={false}
          toastId={t.id}
          updateHeight={handlers.updateHeight}
        />
      ),
      { duration: Infinity },
    );
  };

  return (
    <>
      <Toaster {...(getHotToastConfig() as ToasterProps)} />
      <button onClick={showToast}>Show Toast</button>
    </>
  );
}
```

## Notes

- The toast automatically dismisses after the specified `duration` unless set to `false`
- Hovering over the toast pauses the auto-dismiss timer
- The progress indicator at the bottom shows the remaining time before auto-dismiss
- When `children` is provided, a details toggle button appears to expand/collapse additional content
- The component uses `react-hot-toast` for toast management
- The component requires the `useCountdown` hook for the progress indicator functionality
