ReuseJS Modal

Overview

A Modal component is a user interface element that appears as a popup or dialog box over the main content of a web page. Modals are commonly used to display additional information, forms, alerts, or other types of content that require user attention or interaction.

A Headless Modal is a component that provides the functionality and behaviour of a modal without dictating its appearance or presentation. It is "headless" because it doesn't come with any pre-defined styles or visuals, leaving the responsibility of styling and rendering entirely to the developer. This approach gives you more flexibility and control over how the modal looks and behaves in your application.

Installation

To use the HeadlessModal component, you need to install the following dependencies:

  • @locoworks/reusejs-toolkit-react-hooks: This package provides several reusable React hooks, including useOutsideClicker, useMountComponent, and useClosableComponent.
  • tailwind-merge: A utility library for merging Tailwind CSS classes.
  • framer-motion: A library for adding animations to React components.

Make sure you have these packages installed in your project by running the following command:

npm install @locoworks/reusejs-react-modal
      //or
yarn add @locoworks/reusejs-react-modal

External Dependencies

This code relies on the following external packages:

  • @locoworks/reusejs-toolkit-react-hooks
  • tailwind-merge
  • framer-motion

Ensure that you have these packages available in your project to use the HeadlessModal component.

yarn add @locoworks/reusejs-toolkit-react-hooks tailwind-merge framer-motion
      //or
npm install @locoworks/reusejs-toolkit-react-hooks tailwind-merge framer-motion

Importing

To use the HeadlessModal component, import it into your React components as follows:

import { HeadlessModal } from '@locoworks/reusejs-react-modal';

Types/ Exported Components

The HeadlessModal function takes several parameters and returns a headless modal component. It has the following signature:

function HeadlessModal(config: any, unmountDelay = 200, mountingNode?: any): React.ComponentType<any>;

The config parameter is an object that provides configuration options for the modal component. The unmountDelay parameter is optional and specifies the delay (in milliseconds) before the modal unmounts. The mountingNode parameter is also optional and determines where the modal should be mounted in the DOM.

Props

The HeadlessModal component takes in the following props for styling the backdrop and the modal-wrapper which is used to apply animations to the modal component.

  • modalWrapperClasses(string): [Optional]. These are style classes which will be appplied to the modal wrapper element. These classes will be merged with the default classes.
  • backdropClasses(string): [Optional] These are tailwind styles which will be applied to the backdrop component which acts as a layer between the main page UI and Modal.
  • animations(object): [Optional] HeadlessModal comes with some predefined animation for backdrop and modal. These animations can be furthur customised by the user using this prop. animation prop accept two key value pairs of backdrop and modal, where backdrop and modal are the key and value will be an object with following keys:
    • initial: For enter animation
    • animate: For normal position
    • exit: for exit animation
    • transition: for specifying which animation should be used for showing animation.
  • component: This is the component passed to the HeadlessModal wrapper. This component will be render as the modal and hence you can control exactly how the modal will look by passing your own custom component.
  • disableOutsideClick : This prop is used to control weather the modal will disapper if any outside click is registered. By default this prop is undefined or false. To dissable outside click you can pass this props as true

Modal use framer-motion for animation hence accepts its key:value pairs to create animations. Thus we can use framer-motion

Examples and Usage

Headless Modal Example

/* eslint-disable react/display-name */
import React from "react";
import { HeadlessModal } from "@locoworks/reusejs-react-modal";
import { ReuseButton } from "@locoworks/reusejs-react-button";
import CancelIcon from "../icons/CancelIcon";

const Example = () => {
	const Modal = (props: any, ref: any) => {
		return (
			<div
				ref={ref}
				className="relative bg-white text-black px-2 py-5 rounded border-2 flex flex-col items-center gap-y-5 w-[400px]"
			>
				<div
					className="text-gray-500 bg-transparent absolute top-2 right-2 p-0 cursor-pointer"
					onClick={() => {
						props.onAction(false);
					}}
				>
					<CancelIcon />
				</div>
				<label>This is a sample Modal!</label>
				<ReuseButton
					className="rounded bg-blue-400 px-3 py-1 w-fit"
					onClick={() => {
						props.onAction("Closed");
					}}
				>
					Close
				</ReuseButton>
			</div>
		);
	};

	const showModal = async () => {
		const result = await HeadlessModal({
			component: Modal,
			backdropClasses: "bg-green-500",
			inputValues: {
				input: "Hello",
			},
			animations: {
				modal: {
					initial: { opacity: 0, x: -400, y: -400 },
					animate: { opacity: 1, x: 0, y: 0 },
					exit: { opacity: 0, x: 400, y: 400 },
				},
			},
		});
		console.log(result);
	};

	return (
		<div className="flex flex-col items-center gap-x-3 justify-center py-10 mt-10 border rounded bg-gray-50">
			<ReuseButton className="bg-blue-500 px-2 py-1 mt-3" onClick={showModal}>
				Open modal
			</ReuseButton>
		</div>
	);
};

export default Example;

Confirm Modal Example

/* eslint-disable react/display-name */
import React from "react";
import { HeadlessModal } from "@locoworks/reusejs-react-modal";
import { ReuseButton } from "@locoworks/reusejs-react-button";
import CancelIcon from "../icons/CancelIcon";

const ConfirmExample = () => {
	const Modal = (props: any, ref: any) => {
		return (
			<div
				ref={ref}
				className="relative bg-white text-black px-2 py-8 rounded border-2 flex flex-col items-center gap-y-5 w-[400px]"
			>
				<div
					className="text-gray-500 bg-transparent absolute top-2 right-2 p-0 cursor-pointer"
					onClick={() => {
						props.onAction(false);
					}}
				>
					<CancelIcon />
				</div>
				<label>Are you sure you want to perform this action!!</label>
				<div className="w-1/2 flex justify-between">
					<ReuseButton
						className="rounded bg-red-400 px-3 py-1 w-fit"
						onClick={() => {
							props.onAction(false);
						}}
					>
						Cancel
					</ReuseButton>
					<ReuseButton
						className="rounded bg-green-400 px-3 py-1 w-fit"
						onClick={() => {
							props.onAction(true);
						}}
					>
						Confirm
					</ReuseButton>
				</div>
			</div>
		);
	};
	const showModal = async () => {
		const result = await HeadlessModal({
			component: Modal,
			backdropClasses: "bg-red-500",
		});
		if (result) {
			setTimeout(() => {
				alert("Confirmed");
			}, 500);
		}
		// console.log(result);
	};

	return (
		<div className="flex flex-col items-center gap-x-3 justify-center py-10 mt-10 border rounded bg-gray-50">
			<ReuseButton className="bg-blue-500 px-2 py-1 mt-3" onClick={showModal}>
				Open modal
			</ReuseButton>
		</div>
	);
};

export default ConfirmExample;

Alert Modal Example

/* eslint-disable react/display-name */
import React from "react";
import { HeadlessModal } from "@locoworks/reusejs-react-modal";
import { ReuseButton } from "@locoworks/reusejs-react-button";
import CancelIcon from "../icons/CancelIcon";
import AlertIcon from "../icons/AlertIcon";

const AlertExample = () => {
	const Modal = (props: any, ref: any) => {
		return (
			<div
				ref={ref}
				className="relative bg-red-50 text-red-700 px-2 py-8 rounded-lg border-2 border-red-700 flex flex-col items-center gap-y-5 w-[400px] font-bold text-lg"
			>
				<div
					className="text-gray-500 bg-transparent absolute top-2 right-2 p-0 cursor-pointer"
					onClick={() => {
						props.onAction(false);
					}}
				>
					<CancelIcon />
				</div>
				<AlertIcon />
				<label>This is a sample Alert!!</label>
				<ReuseButton
					className="rounded bg-red-400  text-red-700 px-3 py-1 w-fit hover:bg-red-500 font-bold"
					onClick={() => {
						props.onAction(true);
					}}
				>
					Confirm
				</ReuseButton>
			</div>
		);
	};
	const showModal = async () => {
		const result = await HeadlessModal({
			component: Modal,
			backdropClasses: "bg-black",
		});
		if (result) {
			setTimeout(() => {
				alert("Confirmed");
			}, 500);
		}
	};

	return (
		<div className="flex flex-col items-center gap-x-3 justify-center py-10 mt-10 border rounded bg-gray-50">
			<ReuseButton className="bg-blue-500 px-2 py-1 mt-3" onClick={showModal}>
				Open modal
			</ReuseButton>
		</div>
	);
};

export default AlertExample;

Success Modal Example

/* eslint-disable react/display-name */
import React from "react";
import { HeadlessModal } from "@locoworks/reusejs-react-modal";
import { ReuseButton } from "@locoworks/reusejs-react-button";

const SuccessExample = () => {
	const Modal = (props: any, ref: any) => {
		return (
			<div
				ref={ref}
				className="relative bg-green-300 px-2 py-8 rounded-lg border-2 border-white flex flex-col items-center gap-y-5 w-[400px] font-bold text-lg"
			>
				<label>This is a sample Success Modal</label>
			</div>
		);
	};
	const showModal = async () => {
		const result = await HeadlessModal({
			component: Modal,
			backdropClasses: "bg-black",
		});
		if (result) {
			setTimeout(() => {
				alert("Confirmed");
			}, 500);
		}
	};

	return (
		<div className="flex flex-col items-center gap-x-3 justify-center py-10 mt-10 border rounded bg-gray-50">
			<ReuseButton className="bg-blue-500 px-2 py-1 mt-3" onClick={showModal}>
				Open modal
			</ReuseButton>
		</div>
	);
};

export default SuccessExample;

Input Modal Example

/* eslint-disable react/display-name */
import React from "react";
import { HeadlessModal } from "@locoworks/reusejs-react-modal";
import { ReuseButton } from "@locoworks/reusejs-react-button";
import CancelIcon from "../icons/CancelIcon";

const InputModal = () => {
	const Modal = (props: any, ref: any) => {
		let nameValue = "";
		let emailValue = "";
		return (
			<div
				ref={ref}
				className="relative bg-white px-2 py-8 rounded-lg border-2 border-blue-500 flex flex-col items-center gap-y-5 w-[400px] font-bold text-lg"
			>
				<div
					className="text-gray-500 bg-transparent absolute top-2 right-2 p-0 cursor-pointer"
					onClick={() => {
						props.onAction(false);
					}}
				>
					<CancelIcon />
				</div>
				<label>Please Enter the following Fields</label>
				<input
					className="px-2 py-1 font-normal text-base border-black rounded border"
					placeholder="Enter Name"
					name="name"
					onChange={(e) => {
						nameValue = e.target.value;
					}}
				/>
				<input
					className="px-2 py-1 font-normal text-base border-black rounded border"
					placeholder="Enter E-Mail"
					name="email"
					onChange={(e) => {
						emailValue = e.target.value;
					}}
				/>
				<ReuseButton
					className="px-3 py-1 rounded "
					onClick={() => {
						if (nameValue === "" || emailValue === "") {
							props.onAction(false);
						} else {
							props.onAction({ name: nameValue, email: emailValue });
						}
					}}
				>
					Submit
				</ReuseButton>
			</div>
		);
	};

	const showModal = async () => {
		const result = await HeadlessModal({
			component: Modal,
			backdropClasses: "bg-black",
		});
		console.log("result", result);
	};

	return (
		<div className="flex flex-col items-center gap-x-3 justify-center py-10 mt-10 border rounded bg-gray-50">
			<ReuseButton className="bg-blue-500 px-2 py-1 mt-3" onClick={showModal}>
				Open modal
			</ReuseButton>
		</div>
	);
};

export default InputModal;

Bottom Modal Example

/* eslint-disable react/display-name */
import React, { useState } from "react";
import { HeadlessModal } from "@locoworks/reusejs-react-modal";
import { ReuseButton } from "@locoworks/reusejs-react-button";
import HandleIcon from "../icons/HandleIcon";

const BottomModal = () => {
	const Modal = React.forwardRef((props: any, ref: any) => {
		const [full, setFull] = useState(false);
		return (
			<div
				ref={ref}
				className={
					"w-screen bottom-0 bg-white rounded-t-xl flex flex-col items-center py-3 gap-y-3 transition-all " +
					(full ? " h-screen" : " max-h-[90vh] h-fit ")
				}
			>
				<div
					className={"w-fit h-fit cursor-pointer " + (full ? "" : "rotate-180")}
					onClick={() => {
						setFull(!full);
					}}
				>
					<HandleIcon />
				</div>
				<div className="h-[1px] w-full bg-[#DEDEDE]" />
				<div className="px-4 overflow-y-scroll">
					<h2 className="text-2xl font-bold hover:underline cursor-pointer w-fit">
						What is Lorem Ipsum?
					</h2>
					<p>
						<span className="font-bold">{`Lorem Ipsum`}</span>
						{` is simply dummy text of
					the printing and typesetting industry. Lorem Ipsum has been the
					industry's standard dummy text ever since the 1500s, when an unknown
					printer took a galley of type and scrambled it to make a type specimen
					book. It has survived not only five centuries, but also the leap into
					electronic typesetting, remaining essentially unchanged. It was
					popularised in the 1960s with the release of Letraset sheets
					containing Lorem Ipsum passages, and more recently with desktop
					publishing software like Aldus PageMaker including versions of Lorem
					Ipsum.`}
					</p>
					<h2 className="text-2xl font-bold mt-2 hover:underline cursor-pointer w-fit">
						Why do we use it?
					</h2>
					<p>
						{`It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).`}
					</p>
				</div>
			</div>
		);
	});

	const showModal = async () => {
		const result = await HeadlessModal({
			component: Modal,
			backdropClasses: "bg-black bg-opacity-90",
			inputValues: {
				input: "Hello",
			},
			modalWrapperClasses: "absolute bottom-0",
			animations: {
				modal: {
					initial: { opacity: 0, y: 400 },
					animate: { opacity: 1, y: 0 },
					exit: { opacity: 0, y: 400 },
					transition: { ease: "easeIn" },
				},
			},
		});
		console.log(result);
	};

	return (
		<div className="flex flex-col items-center gap-x-3 justify-center py-10 mt-10 border rounded bg-gray-50">
			<ReuseButton className="bg-blue-500 px-2 py-1 mt-3" onClick={showModal}>
				Open modal
			</ReuseButton>
		</div>
	);
};

export default BottomModal;

Disabled Outside Click Modal Example

/* eslint-disable react/display-name */
import React from "react";
import { HeadlessModal } from "@locoworks/reusejs-react-modal";
import { ReuseButton } from "@locoworks/reusejs-react-button";
import CancelIcon from "../icons/CancelIcon";

const DisabledOutsideClick = () => {
	const Modal = (props: any, ref: any) => {
		return (
			<div
				ref={ref}
				className="relative bg-white text-black px-2 py-5 rounded border-2 flex flex-col items-center gap-y-5 w-[400px]"
			>
				<div
					className="text-gray-500 bg-transparent absolute top-2 right-2 p-0 cursor-pointer"
					onClick={() => {
						props.onAction(false);
					}}
				>
					<CancelIcon />
				</div>
				<label>This is a sample Modal!</label>
				<label>{`Saying to you ${props.inputValues.input}`}</label>
				<ReuseButton
					className="rounded bg-blue-400 px-3 py-1 w-fit"
					onClick={() => {
						props.onAction("Closed");
					}}
				>
					Close
				</ReuseButton>
			</div>
		);
	};

	const showModal = async () => {
		const result = await HeadlessModal({
			component: Modal,
			backdropClasses: "bg-gray-500",
			disableOutsideClick: true,
			inputValues: {
				input: "Hello",
			},
			animations: {
				modal: {
					initial: { opacity: 0, x: -400, y: -400 },
					animate: { opacity: 1, x: 0, y: 0 },
					exit: { opacity: 0, x: 400, y: 400 },
				},
			},
		});
		console.log(result);
	};

	return (
		<div className="flex flex-col items-center gap-x-3 justify-center py-10 mt-10 border rounded bg-gray-50">
			<ReuseButton className="bg-blue-500 px-2 py-1 mt-3" onClick={showModal}>
				Open modal
			</ReuseButton>
		</div>
	);
};

export default DisabledOutsideClick;

Styling

The modal backdrop uses default tailwind CSS classes for styling, but you can customize it by passing additional classes through the backdropClasses property in the modalConfig object.

The modal content can be styled within the ModalContent component using regular CSS or any styling library of your choice.

Best Practices

  • Ensure that the @locoworks/reusejs-toolkit-react-hooks, tailwind-merge, and framer-motion dependencies are installed and available in your project.
  • Avoid directly modifying the props object received by the ModalContent component, as it may contain configuration options for the modal.