---
title: Loader
description: A fullscreen loading indicator with animated wave effect and custom label, displayed on a backdrop overlay
---

```tsx
'use client';

import { useState } from 'react';

import { Button } from '@/ui/buttons/Button';
import { Loader } from '@/ui/Loader';

export function LoaderDemo() {
  const [isLoading, setIsLoading] = useState(false);

  return (
    <div>
      <Button onClick={() => setIsLoading(true)}>Show Loader</Button>
      <Loader isVisible={isLoading} label="Loading content, please wait..." />
    </div>
  );
}
```
<div id="backdrop" />

## Installation

Using the CLI:

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

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

```tsx
'use client';

import clsx from 'clsx';
import { useEffect, type ComponentProps, type FunctionComponent } from 'react';
import { useTransitionState } from 'react-transition-state';

import { Backdrop } from './Backdrop';

export interface LoaderProps {
  isVisible?: boolean;
  label?: string;
  portalId?: ComponentProps<typeof Backdrop>['target'];
}

const loaderSVG = (
  <svg viewBox="0 0 140 140" width={140}>
    <defs>
      <path
        id="a"
        d="m70 74.1-9.6 9.6 9.6 9.5 9.5-9.5L70 74Zm6.6-70.7c-.7-2.7-6.9 1.1-20.1 13 5.3 5.8 7.9 2.5 12.1 1 3.4 6.4 4.8 35.4-17.3 44 .6-29.1.9-60.4.9-60.4a1 1 0 0 0-1.3-1c-6.1.8-17 11.6-25.3 20.6 4.6 7.5 10.4.8 15-1.1 0 1.2-1 22.8-.6 44.7C17.3 66 21 19.5 22.2 13.6 22.8 8.9 7.6 19.3 0 26.4c2.4 4.3 5.7 3.4 10 1.8C8.3 49 23 67.9 40 67.5c.6 21.4 2.9 42.2 9 43.7 1 .2 1.6-21.8 2-46.1C92 52.4 76.5 3.4 76.5 3.4"
      />
    </defs>
    <g fill="none" fillRule="evenodd" transform="translate(27 14)">
      <mask id="b" fill="#fff">
        <use xlinkHref="#a" />
      </mask>
      <g mask="url(#b)">
        <path
          fill="#1E2643"
          fillRule="nonzero"
          d="M-83.1-23s6.7 3 11.2 3c4.6 0 6.7-3 11.3-3 4.5 0 6.7 3 11.2 3 4.6 0 6.7-3 11.3-3 4.5 0 6.7 3 11.2 3 4.6 0 6.7-3 11.3-3 4.5 0 6.7 3 11.2 3C.2-20 2.3-23 7-23c4.5 0 6.7 3 11.2 3 4.6 0 6.7-3 11.3-3 4.5 0 6.7 3 11.2 3 4.6 0 6.7-3 11.3-3 4.5 0 6.7 3 11.2 3 4.6 0 6.7-3 11.3-3 4.5 0 6.7 3 11.2 3 4.6 0 11.3-3 11.3-3v138H-83V-23Z"
          className="animate-wave"
        />
        <path
          fill="#1E2643"
          fillRule="nonzero"
          d="m70 74.1.7-.7-.7-.7-.7.7.7.7Zm-9.6 9.6-.7-.7-.7.7.7.7.7-.7Zm9.6 9.5-.7.7.7.7.7-.7-.7-.7Zm9.5-9.5.7.7.7-.7-.7-.7-.7.7Zm-23-67.3-.7-.7-.7.6.7.8.7-.7Zm12.1 1 1-.5-.5-.8-.8.3.3 1Zm-17.3 44h-1V63l1.4-.6-.4-1ZM52.2 1h1-1Zm-1.3-1v1h.2L51 0ZM25.6 20.6l-.7-.7-.5.5.4.7.8-.5Zm15-1.1h1V18l-1.4.5.4 1ZM40 64.2l.1 1h1v-1h-1ZM22.2 13.6l1 .2-1-.2ZM0 26.4l-.7-.8-.6.6.4.7.9-.5Zm10 1.8h1v-1.5l-1.4.6.3.9Zm30 39.3h1v-1h-1v1Zm9 43.7.3-1-.2 1Zm2-46.1-.2-1-.7.2v.8h1Zm18.2 8.3L59.7 83l1.4 1.4 9.6-9.6-1.4-1.4Zm-9.6 11 9.6 9.5 1.4-1.4L61 83l-1.4 1.4Zm11 9.5 9.5-9.5-1.4-1.4-9.5 9.5 1.4 1.4Zm9.5-11-9.5-9.5-1.4 1.4 9.5 9.6 1.4-1.4ZM77.6 3.2a2 2 0 0 0-1-1.3c-.6-.3-1.2-.3-1.8-.2-1 .2-2.4 1-4.1 2-3.4 2.1-8.2 6.1-14.9 12l1.4 1.6c6.6-6 11.4-9.9 14.6-12a11 11 0 0 1 3.4-1.6h.4l2-.5Zm-21.8 14c1.3 1.5 2.6 2.5 3.8 3a6 6 0 0 0 3.5.5c1-.1 2-.6 3-1 1-.5 1.9-1 2.9-1.3l-.7-1.9c-1.1.4-2.2 1-3 1.4l-2.5.9a4 4 0 0 1-2.4-.4 12 12 0 0 1-3.2-2.6l-1.4 1.4Zm12 .7c.7 1.5 1.4 4.3 1.7 8 .2 3.7 0 8-1 12.6-2 8.9-7 18-17.6 22.2l.8 1.8a31.3 31.3 0 0 0 18.8-23.6 51 51 0 0 0 1-13.1c-.3-3.8-1-7-2-8.8l-1.7 1ZM52.3 61.5a5227.3 5227.3 0 0 0 .9-60.1V1h-2v1.3a4268.6 4268.6 0 0 1-1 59l2 .1ZM53.2 1c0-.3 0-.6-.2-.9l-1.8 1h2ZM53 .1a2 2 0 0 0-.6-.7L51.2 1 53 .1Zm-.6-.7a2 2 0 0 0-.9-.4l-.3 2 1.2-1.6Zm-.9-.4a2 2 0 0 0-1 0l.7 2 .3-2Zm-.8 0C49-.6 47 .3 45 1.6s-4.3 3-6.6 5c-4.5 4-9.3 8.9-13.5 13.4l1.5 1.4c4.1-4.5 8.9-9.4 13.3-13.3C42 6 44.1 4.4 46 3.2c2-1.3 3.7-2 5-2.2l-.3-2Zm-26 22c1.3 2.1 2.7 3.3 4.3 3.8a7 7 0 0 0 4.5-.3c1.4-.5 2.8-1.4 4-2.1 1.3-.8 2.4-1.6 3.5-2l-.8-1.9-3.7 2.2c-1.3.7-2.5 1.5-3.7 2-1.2.4-2.3.5-3.2.2-1-.3-2-1.1-3.1-2.8l-1.7 1Zm15-1.5c0 1.1-1.2 22.8-.7 44.7h2c-.5-22 .6-43.4.6-44.7h-2Zm.2 43.7c-5.2.4-9-2-11.6-6-2.7-4-4.3-9.6-5.2-15.6-1.8-12-.5-25 .1-27.8l-2-.4c-.6 3-1.9 16.3-.1 28.5.9 6.1 2.6 12.1 5.5 16.5a14 14 0 0 0 13.5 6.8l-.2-2ZM23.2 13.7l-.1-1.2a2 2 0 0 0-1-1c-.7-.2-1.6 0-2.3.1a28 28 0 0 0-6.2 3.1 106 106 0 0 0-14.3 11L.7 27c3.7-3.5 9.4-7.8 14-10.7a26.4 26.4 0 0 1 6.4-3h.3l-.2-.1v.2l2 .2ZM-.8 27c.6 1.1 1.3 2 2.2 2.6a5 5 0 0 0 2.8 1c2 .1 4-.6 6-1.4l-.6-1.8c-2.1.8-3.8 1.3-5.2 1.2-.7 0-1.3-.2-1.9-.6-.5-.4-1-1-1.6-2l-1.8 1Zm9.6 1a41.8 41.8 0 0 0 8.4 28.3c5.7 7.5 14 12.3 22.9 12v-2a26 26 0 0 1-21.3-11.2 40 40 0 0 1-8-27H9ZM39 67.4c.3 10.7 1 21.3 2.5 29.5.7 4 1.6 7.5 2.7 10.1 1.1 2.6 2.6 4.6 4.7 5l.4-2c-1-.2-2.2-1.3-3.2-3.8-1-2.3-2-5.7-2.7-9.7-1.4-8-2-18.5-2.4-29.1h-2Zm9.8 44.6c.7.2 1.1-.3 1.2-.4l.2-.4.2-.9.3-2.8.5-10.2.9-32.3h-2a1859.2 1859.2 0 0 1-1.4 42.3 38 38 0 0 1-.4 3.2c0-.1.4-.6 1-.4l-.5 2Zm2.6-46a39.2 39.2 0 0 0 21.8-15.4 46 46 0 0 0 7-21.1 73 73 0 0 0-2.6-25.9l-.1-.4v-.1l-1 .2-1 .3a14.2 14.2 0 0 1 .2.6 51 51 0 0 1 1.6 7.1 71 71 0 0 1 .9 18A37.2 37.2 0 0 1 50.8 64l.6 2Z"
          mask="url(#a)"
        />
      </g>
    </g>
  </svg>
);

export const Loader: FunctionComponent<LoaderProps> = ({ isVisible, label, portalId }) => {
  const [{ status, isMounted }, toggle] = useTransitionState({
    timeout: { enter: 250, exit: 250 },
    mountOnEnter: true,
    unmountOnExit: true,
    preEnter: true,
  });

  useEffect(() => {
    toggle(isVisible);
  }, [isVisible, toggle]);

  const entering = status === 'preEnter' || status === 'entering';
  const exiting = status === 'exiting';

  return (
    <Backdrop isVisible={isVisible} target={portalId} sweep>
      {isMounted && (
        <div
          className={clsx('flex flex-col items-center transition-all duration-500', {
            '-translate-x-full opacity-0': entering,
            'translate-x-full opacity-100': exiting,
          })}
          role="alert"
        >
          <div
            className="relative isolate"
            style={{
              maskImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2 2'%3E%3Cellipse cx='1' cy='1' rx='1' ry='1'/%3E%3C/svg%3E")`,
              WebkitMaskImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2 2'%3E%3Cellipse cx='1' cy='1' rx='1' ry='1'/%3E%3C/svg%3E")`,
            }}
          >
            {loaderSVG}
          </div>
          <div
            className={clsx(
              'mx-auto max-w-[320px] px-20 text-center text-b3 transition-all delay-100 duration-400',
              {
                '-translate-x-full opacity-0': entering,
                'translate-x-full opacity-100': exiting,
              },
            )}
          >
            {label}
          </div>
        </div>
      )}
    </Backdrop>
  );
};
```

## Usage

```tsx showLineNumbers
'use client';

import { useState } from 'react';
import { Loader } from '@/docs/lib/ui/loader';
import { Button } from '@/docs/lib/ui/Button';

export function Component() {
  const [isLoading, setIsLoading] = useState(false);

  const handleLoadData = async () => {
    setIsLoading(true);
    // Simulate async operation
    await fetch('/api/data');
    setIsLoading(false);
  };

  return (
    <>
      <Button onClick={handleLoadData}>Load Data</Button>
      <Loader isVisible={isLoading} label="Loading your data..." />
    </>
  );
}
```

## API Reference

### Loader

| Prop        | Type      | Default     | Description                                       |
| ----------- | --------- | ----------- | ------------------------------------------------- |
| `isVisible` | `boolean` | `undefined` | Controls the visibility of the loader             |
| `label`     | `string`  | `undefined` | Optional text message displayed below the spinner |

## Notes

- **Fullscreen Overlay**: Renders on a `Backdrop` component that covers the entire viewport with a sweep animation
- **Animated Wave Effect**: Features a custom SVG animation with an animated wave using CSS keyframes
- **Transition States**: Uses `react-transition-state` for smooth enter/exit animations with slide effects
- **Accessibility**: Includes `role="alert"` for screen reader announcements when loading begins
- **Portal Rendering**: Rendered in a portal through the Backdrop component for proper z-index layering
- **Unmount on Exit**: Automatically unmounts from the DOM when hidden to improve performance
- **Custom Label**: Supports optional text to inform users about the loading process
- **Client Component**: Must be used in client components (requires "use client" directive)
