---
title: Popin
description: A modal dialog component that displays content in a centered overlay with smooth animations and backdrop support
---

```tsx
'use client';

import { useState } from 'react';
import { Popin } from '@/ui/Popin';
import { Button } from '@/ui/buttons/Button';

export function PopinDemo() {
  const [isVisible, setIsVisible] = useState(false);

  return (
    <div>
      <Button onClick={() => setIsVisible(true)}>Open Popin</Button>
      <Popin
        isVisible={isVisible}
        onClose={() => setIsVisible(false)}
        title="Welcome!"
        closeLabel="Close"
      >
        <p>
          This is a modal dialog that overlays the page content. Click the backdrop or the close
          button to dismiss it.
        </p>
      </Popin>
    </div>
  );
}
```
<div id="backdrop" />


## Installation

Using the CLI:

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

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

```tsx
import type { FunctionComponent, PropsWithChildren, ReactNode } from 'react';
import { useRef } from 'react';
import { twMerge } from './helpers/twMerge';
import { Backdrop, type BackdropProps } from './Backdrop';
import { Button } from './buttons/Button';
import clsx from 'clsx';
import { useSlots } from './hooks/useSlots';

export type ClosePopinCallback = () => void;

export interface PopinFooterProps {
  closeLabel?: string;
  onClose: ClosePopinCallback;
}

const PopinFooter = ({ closeLabel, onClose }: PopinFooterProps) => {
  return (
    <Button
      color="black"
      variant="circle"
      icon="CrossDefault"
      className="mx-auto"
      aria-label={closeLabel}
      onClick={onClose}
    />
  );
};

export interface PopinProps {
  title?: ReactNode;
  closeLabel?: string;
  onClose: ClosePopinCallback;
  isVisible: boolean;
  className?: string;
  bodyClassName?: string;
  showCloseButton?: boolean;
  target?: BackdropProps['target'];
  /**
   * Whether the popin can be dismissed via ESC, backdrop click, or close button.
   * When false, only programmatic closing via onClose is allowed.
   * @default true
   */
  dismissable?: boolean;
  /**
   * @deprecated use popinFooter slot instead
   */
  Footer?: FunctionComponent<PopinFooterProps> | false;
}

export const Popin: FunctionComponent<PropsWithChildren<PopinProps>> = ({
  title,
  children,
  closeLabel,
  onClose,
  isVisible,
  className = 'sm:w-360',
  bodyClassName,
  Footer = PopinFooter,
  dismissable = true,
}) => {
  const { children: popinBody, popinFooter } = useSlots(children, ['popinFooter']);

  // Prevent initial mount flash when closed: only animate leave if it was visible before
  const wasVisibleRef = useRef(isVisible);
  const wasVisible = wasVisibleRef.current;
  const isLeaving = !isVisible && wasVisible;

  // update ref for next render
  wasVisibleRef.current = isVisible;
  return (
    <Backdrop
      isVisible={isVisible}
      onClose={dismissable ? onClose : undefined}
      dismissable={dismissable}
    >
      <div
        className={twMerge(
          clsx(
            'border-lightGrey rounded-16 pointer-events-auto mx-20 w-full border bg-white origin-center will-change[transform,opacity]',
            {
              'animate-zoom-in': isVisible,
              'animate-zoom-out': !isVisible && isLeaving,
              'opacity-0 scale-90': !isVisible && !isLeaving,
            },
          ),
          className,
        )}
      >
        <div
          className={twMerge('text-center max-h-[90vh] flex flex-col p-40 gap-40', bodyClassName)}
        >
          {title && <div className="text-h5 mt-0 -mb-20 text-start font-serif">{title}</div>}
          <div className="text-start overflow-auto">{popinBody}</div>
          {!popinFooter?.length && Footer && dismissable && (
            <div>
              <Footer closeLabel={closeLabel} onClose={onClose} />
            </div>
          )}

          {popinFooter}
        </div>
      </div>
    </Backdrop>
  );
};
```

## Usage

```tsx showLineNumbers
'use client';

import { useState } from 'react';
import { Popin } from '@/docs/lib/ui/popin';
import { Button } from '@/docs/lib/ui/Button';

export function Component() {
  const [isVisible, setIsVisible] = useState(false);

  return (
    <div>
      <Button onClick={() => setIsVisible(true)}>Open Dialog</Button>
      <Popin
        isVisible={isVisible}
        onClose={() => setIsVisible(false)}
        title="Confirmation"
        closeLabel="Close dialog"
      >
        <p>Are you sure you want to proceed with this action?</p>
      </Popin>
    </div>
  );
}
```

## API Reference

### Popin

| Prop            | Type          | Default      | Description                                                                                                  |
| --------------- | ------------- | ------------ | ------------------------------------------------------------------------------------------------------------ |
| `isVisible`     | `boolean`     | (required)   | Controls the visibility of the modal. When `true`, the modal is displayed with entrance animations.          |
| `onClose`       | `() => void`  | (required)   | Callback function invoked when the user closes the modal by clicking the backdrop or close button.           |
| `title`         | `ReactNode`   | `undefined`  | Optional title displayed at the top of the modal. Can be a string or any React element.                      |
| `closeLabel`    | `string`      | `undefined`  | Accessible label for the close button, used for `aria-label` attribute. Important for screen readers.        |
| `className`     | `string`      | `"sm:w-360"` | Custom CSS classes for the modal container. The default width is 360px on small screens and above.           |
| `bodyClassName` | `string`      | `undefined`  | Custom CSS classes for the modal body container that wraps the title, content, and footer.                   |
| `target`        | `HTMLElement` | `undefined`  | Optional portal target element where the modal should be rendered. Uses document.body by default.            |
| `dismissable`   | `boolean`     | `true`       | When `false`, prevents the modal from being closed via ESC key, backdrop click, or close button. Only programmatic closing via `onClose` is allowed. |
| `children`      | `ReactNode`   | `undefined`  | Content to display inside the modal. Supports slot-based footer customization via `data-slot="popinFooter"`. |

## Notes

- **Backdrop Integration**: The Popin component uses the `Backdrop` component internally to provide a fullscreen overlay. Clicking the backdrop will trigger the `onClose` callback (unless `dismissable={false}`).

- **Dismissable Control**: Use the `dismissable` prop to control whether users can close the modal. When `dismissable={false}`, the modal cannot be closed via ESC key, backdrop click, or the close button, forcing programmatic closure via the `onClose` callback.

- **Smooth Animations**: Features entrance (`animate-zoomIn`) and exit (`animate-zoomOut`) animations for a polished user experience. The modal scales and fades smoothly during transitions.

- **Slot-Based Footer**: By default, a close button is rendered in the footer (when `dismissable={true}`). You can customize the footer by passing elements with `data-slot="popinFooter"`:

  ```tsx
  <Popin isVisible={isVisible} onClose={onClose} title="Custom Footer">
    <p>Modal content goes here</p>
    <div data-slot="popinFooter">
      <Button onClick={onClose}>Cancel</Button>
      <Button onClick={handleConfirm}>Confirm</Button>
    </div>
  </Popin>
  ```

- **Accessibility**: The close button uses the `closeLabel` prop for `aria-label`, ensuring screen readers can identify its purpose. The backdrop has `role="presentation"` and `aria-hidden="true"`.

- **Responsive Design**: The modal is responsive with default padding and width that adapts to different screen sizes. The content area has a max-height of 90vh with overflow scrolling.

- **Portal Rendering**: The modal is rendered in a React Portal (via the Backdrop component), ensuring it appears above all other page content with proper stacking context.

- **Client Component**: This is a client component (marked with `"use client"`) due to interactive state management and animations.

- **Animation Optimization**: The component includes logic to prevent initial mount flash when closed, only animating the leave transition if the modal was previously visible.
