JSFiddle - React, Tailwind, and code Playground

by Pranab Dey

SCSS

/* Variables */
$backdrop-color: rgba(0, 0, 0, 0.4);
$surface: #ffffff;
$text: #1f2937;
$border: #e5e7eb;
$shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
$radius: 12px;
$z-backdrop: 999;
$z-modal: 1000;

/* Keyframes */
@keyframes modalIn {
    from {
        opacity: 0;
        transform: translateY(8px) scale(0.98);
    }

    to {
        opacity: 1;
        transform: translateY(0) scale(1);
    }
}

@keyframes modalOut {
    from {
        opacity: 1;
        transform: translateY(0) scale(1);
    }

    to {
        opacity: 0;
        transform: translateY(8px) scale(0.98);
    }
}

/* Backdrop */
.backdrop {
    position: fixed;
    inset: 0;
    background: $backdrop-color;
    transition: opacity 180ms ease;
    z-index: $z-backdrop;
    pointer-events: none; // disabled unless open
    opacity: 0;
}

.backdrop_open {
    opacity: 1;
    pointer-events: auto;
}

.backdrop_closed {
    opacity: 0;
    pointer-events: none;
}

h2 {
    margin-block-start: 0.6rem;
    margin-block-end: 0.6rem;
}

/* Modal container */
.modal {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) !important; // slight offset to feel snappy
    background: $surface;
    color: $text;
    width: 90vw; // default responsive base
    max-width: 760px; // overridden by size variants
    max-height: 85vh;
    border-radius: $radius;
    box-shadow: $shadow;
    overflow: hidden; // clip rounded corners
    display: grid;
    grid-template-rows: auto 1fr auto;
    z-index: $z-modal;
    opacity: 0;
    pointer-events: none;
    border: 1px solid $border;
}

.modal_open {
    animation: modalIn 180ms ease forwards;
    pointer-events: auto;
}

.modal_closed {
    animation: modalOut 160ms ease forwards;
    pointer-events: none;
}

.lockScroll {
    overflow: hidden;
}

/* Size variants */
.modal_xs {
    max-width: 425px;
}

.modal_sm {
    max-width:...

TypeScript

import React, { useCallback, useState, useEffect, useMemo, useRef } from "react";
import { createPortal } from "react-dom";
import styles from "./Modal.module.scss";

type ModalSize = "xs" | "sm" | "md" | "lg" | "xl";
type CloseButtonAlignment = "right" | "center" | "left";
type ModalRole = "dialog" | "alertdialog";
type ModalVariant = "default" | "bottom-sheet";
type CloseReason = "backdrop" | "escape" | "programmatic" | "action:closeButton" | "action:footerClose";

type HFType = {
  active?: boolean;
  title?: React.ReactNode;
  id?: string;
  className?: string;
  alignment?:string;
};

interface ModalProps {
  size?: ModalSize;
  open: boolean;
  close: () => void;
  variant?: ModalVariant;
  preventClose?:boolean;
  onCloseReason?: (reason: CloseReason) => void;
  header?: HFType;
  footer?: HFType;
  showTopClose?: boolean;
  showFooterCloseButton?: boolean;
  closeButtonAlignment?: CloseButtonAlignment;
  closeButtonFullWidth?: boolean;
  fullScreen?: boolean;
  closeOnBackdropClick?: boolean;
  closeOnEsc?: boolean;
  modalId?: string;
  contentId?: string;
  backdropId?: string;
  modalClassName?: string;
  contentClassName?: string;
  backdropClassName?: string;
  modalStyle?: React.CSSProperties;
  contentStyle?: React.CSSProperties;
  backdropStyle?: React.CSSProperties;
  initialFocusRef?: React.RefObject<HTMLElement>;
  restoreFocus?: boolean;
  inertBackground?: boolean;
  ariaLabel?: string;
  role?: ModalRole;
  portal?: HTMLElement | null;
  children?: React.ReactNode;
}

const defaultHeaderVal = {active : true, title : "Modal Header", id : "", className : "", alignment: "left"};
const defaultFooterVal = {active : true, title : "Modal Footer", id : "", className : "", alignment: "left"};

const Modal: React.FC<ModalProps> = ({
  size = "md",
  open = false,
  variant = "default",
  close,
  header = {active : true, title : "Modal Header", id : "", className : "", alignment: "left"},
 ...