diff --git a/services/ui-src/src/components/index.ts b/services/ui-src/src/components/index.ts
index abe09ae5..5a947975 100644
--- a/services/ui-src/src/components/index.ts
+++ b/services/ui-src/src/components/index.ts
@@ -49,6 +49,8 @@ export { ExportedReportPage } from "./pages/Export/ExportedReportPage";
// menus
export { Menu } from "./menus/Menu";
export { MenuOption } from "./menus/MenuOption";
+// modals
+export { Modal } from "./modals/Modal";
// Redirects
export { PostLogoutRedirect } from "./PostLogoutRedirect/index";
// tables
diff --git a/services/ui-src/src/components/modals/Modal.test.tsx b/services/ui-src/src/components/modals/Modal.test.tsx
new file mode 100644
index 00000000..2aa78aff
--- /dev/null
+++ b/services/ui-src/src/components/modals/Modal.test.tsx
@@ -0,0 +1,57 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { axe } from "jest-axe";
+//components
+import { Text } from "@chakra-ui/react";
+import { Modal } from "components";
+
+const mockCloseHandler = jest.fn();
+const mockConfirmationHandler = jest.fn();
+
+const content = {
+ heading: "Dialog Heading",
+ body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed accumsan diam vitae metus lacinia, eget tempor purus placerat.",
+ actionButtonText: "Dialog Action",
+ closeButtonText: "Cancel",
+};
+
+const modalComponent = (
+
+ {content.body}
+
+);
+
+describe("Test Modal", () => {
+ beforeEach(() => {
+ render(modalComponent);
+ });
+
+ test("Modal shows the contents", () => {
+ expect(screen.getByText(content.heading)).toBeTruthy();
+ expect(screen.getByText(content.body)).toBeTruthy();
+ });
+
+ test("Modals action button can be clicked", () => {
+ fireEvent.click(screen.getByText(/Dialog Action/i));
+ expect(mockConfirmationHandler).toHaveBeenCalledTimes(1);
+ });
+
+ test("Modals close button can be clicked", () => {
+ fireEvent.click(screen.getByText(/Cancel/i));
+ expect(mockCloseHandler).toHaveBeenCalledTimes(1);
+ });
+});
+
+describe("Test Modal accessibility", () => {
+ it("Should not have basic accessibility issues", async () => {
+ const { container } = render(modalComponent);
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+});
diff --git a/services/ui-src/src/components/modals/Modal.tsx b/services/ui-src/src/components/modals/Modal.tsx
new file mode 100644
index 00000000..f90f7c55
--- /dev/null
+++ b/services/ui-src/src/components/modals/Modal.tsx
@@ -0,0 +1,183 @@
+import { ReactNode } from "react";
+import {
+ Box,
+ Button,
+ Flex,
+ Heading,
+ Image,
+ Modal as ChakraModal,
+ ModalBody,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ Spinner,
+} from "@chakra-ui/react";
+import closeIcon from "assets/icons/close/icon_close_primary.svg";
+
+export const Modal = ({
+ modalDisclosure,
+ content,
+ onConfirmHandler,
+ submitting,
+ formId,
+ children,
+}: Props) => {
+ return (
+
+
+
+
+
+ {content.heading}
+
+
+ {content.subheading && (
+ {content.subheading}
+ )}
+
+ }
+ variant="link"
+ onClick={modalDisclosure.onClose}
+ >
+ Close
+
+
+ {children}
+
+ {formId && (
+
+ )}
+ {onConfirmHandler && (
+
+ )}
+ {content.closeButtonText && (
+
+ )}
+
+
+
+ );
+};
+
+interface Props {
+ modalDisclosure: {
+ isOpen: boolean;
+ onClose: any;
+ };
+ content: {
+ heading: string;
+ subheading?: string;
+ actionButtonText: string | ReactNode;
+ closeButtonText?: string;
+ };
+ submitting?: boolean;
+ onConfirmHandler?: Function;
+ formId?: string;
+ children?: ReactNode;
+ [key: string]: any;
+}
+
+const sx = {
+ modalContent: {
+ boxShadow: ".125rem .125rem .25rem",
+ borderRadius: "0",
+ maxWidth: "30rem",
+ marginX: "4rem",
+ padding: "2rem",
+ },
+ modalHeader: {
+ padding: "0",
+ },
+ modalHeaderText: {
+ padding: "0 4rem 0 0",
+ fontSize: "2xl",
+ fontWeight: "bold",
+ },
+ modalSubheader: {
+ margin: "0.5rem auto -1rem auto",
+ },
+ modalCloseContainer: {
+ alignItems: "center",
+ justifycontent: "center",
+ flexShrink: "0",
+ position: "absolute",
+ top: "2rem",
+ right: "2rem",
+ },
+ modalClose: {
+ span: {
+ margin: "0.25rem",
+ paddingTop: "0.06rem",
+ svg: {
+ fontSize: "xs",
+ width: "xs",
+ height: "xs",
+ },
+ },
+ },
+ modalBody: {
+ paddingX: "0",
+ paddingY: "1rem",
+ },
+ modalFooter: {
+ justifyContent: "flex-start",
+ padding: "0",
+ paddingTop: "2rem",
+ },
+ action: {
+ justifyContent: "center",
+ marginRight: "2rem",
+ minWidth: "10rem",
+ span: {
+ marginLeft: "0.5rem",
+ marginRight: "-0.25rem",
+ "&.ds-c-spinner": {
+ marginLeft: 0,
+ },
+ },
+ ".mobile &": {
+ fontSize: "sm",
+ },
+ },
+ close: {
+ justifyContent: "start",
+ padding: ".5rem 1rem",
+ span: {
+ marginLeft: "0rem",
+ marginRight: "0.5rem",
+ },
+ ".mobile &": {
+ fontSize: "sm",
+ marginRight: "0",
+ },
+ },
+ closeIcon: {
+ width: "0.75rem",
+ },
+};