-
Example!
-
-
Counter: {count}
-
-
-
-
-
-
- );
-}
-
-export default Example;
diff --git a/frontend/packages/ui/src/lib/modal/Modal.module.scss b/frontend/packages/ui/src/lib/modal/Modal.module.scss
new file mode 100644
index 0000000000..b5ab9bf29e
--- /dev/null
+++ b/frontend/packages/ui/src/lib/modal/Modal.module.scss
@@ -0,0 +1,113 @@
+@use "ds/_borders" as *;
+@use "ds/_sizes" as *;
+@use "ds/_utils" as *;
+
+.overlay {
+ position: fixed;
+ inset: 0;
+ background: var(--color-overlay-default);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: var(--z-index-set);
+
+ &[data-entering] {
+ animation: overlay-fade-in 0.2s ease-out;
+ }
+
+ &[data-exiting] {
+ animation: overlay-fade-out 0.15s ease-in;
+ }
+}
+
+.modal {
+ position: relative;
+ inline-size: 100%;
+ margin: var(--sp-l);
+ background: var(--color-background-primary);
+ border-radius: $br-8;
+ box-shadow: var(--el-shadow-dark);
+ outline: none;
+ max-block-size: calc(100dvh - var(--sp-xxxl) * 2);
+ display: flex;
+ flex-direction: column;
+
+ &[data-entering] {
+ animation: modal-zoom-in 0.2s ease-out;
+ }
+
+ &[data-exiting] {
+ animation: modal-zoom-out 0.15s ease-in;
+ }
+}
+
+.modalSmall {
+ max-inline-size: $sz-400;
+}
+
+.modalMedium {
+ max-inline-size: $sz-512;
+}
+
+.modalLarge {
+ max-inline-size: px2rem(640);
+}
+
+.modalXlarge {
+ max-inline-size: px2rem(960);
+}
+
+.dialog {
+ position: relative;
+ outline: none;
+ display: flex;
+ flex-direction: column;
+ min-block-size: 0;
+ overflow: hidden;
+ max-block-size: inherit;
+}
+
+
+@keyframes overlay-fade-in {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes overlay-fade-out {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ }
+}
+
+@keyframes modal-zoom-in {
+ from {
+ opacity: 0;
+ transform: scale(0.95);
+ }
+
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+@keyframes modal-zoom-out {
+ from {
+ opacity: 1;
+ transform: scale(0.95);
+ }
+
+ to {
+ opacity: 0;
+ transform: scale(1);
+ }
+}
diff --git a/frontend/packages/ui/src/lib/modal/Modal.tsx b/frontend/packages/ui/src/lib/modal/Modal.tsx
new file mode 100644
index 0000000000..6d236d4d96
--- /dev/null
+++ b/frontend/packages/ui/src/lib/modal/Modal.tsx
@@ -0,0 +1,66 @@
+import {
+ DialogTrigger,
+ ModalOverlay,
+ Modal as RACModal,
+ Dialog,
+} from 'react-aria-components';
+import {
+ createContext,
+ useContext,
+ type ReactNode,
+} from 'react';
+import styles from './Modal.module.scss';
+
+const ModalCloseContext = createContext<(() => void) | null>(null);
+
+export function useModalClose(): (() => void) | null {
+ return useContext(ModalCloseContext);
+}
+
+interface ModalProps {
+ isOpen?: boolean;
+ onOpenChange?: (isOpen: boolean) => void;
+ children: ReactNode;
+ trigger?: ReactNode;
+ isDismissable?: boolean;
+ size?: 'small' | 'medium' | 'large' | 'xlarge';
+ className?: string;
+}
+
+export function Modal({
+ isOpen,
+ onOpenChange,
+ children,
+ trigger,
+ isDismissable = true,
+ size = 'medium',
+ className,
+}: ModalProps) {
+ const sizeClass =
+ size === 'small'
+ ? styles.modalSmall
+ : size === 'large'
+ ? styles.modalLarge
+ : size === 'xlarge'
+ ? styles.modalXlarge
+ : styles.modalMedium;
+
+ return (
+