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"},
...