Modal
Modal
접근 가능한 오버레이 다이얼로그 컴포넌트예요.
출처: 문서
본문
사용법 (Usage)
Modal로 접근 가능한 오버레이 다이얼로그를 만들어요. 보통 useDisclosure 훅으로 열림 상태를 관리해요.
import { useDisclosure } from '@mantine/hooks';
import { Modal, Button } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Modal opened={opened} onClose={close} title="Modal title">
{/* Modal content */}
</Modal>
<Button onClick={open}>Open modal</Button>
</>
);
}
모달 세로 중앙 정렬 (Center modal vertically)
centered prop으로 모달을 세로로 중앙 정렬할 수 있어요.
import { useDisclosure } from '@mantine/hooks';
import { Modal, Button } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Modal opened={opened} onClose={close} title="Centered" centered>
{/* Modal content */}
</Modal>
<Button onClick={open}>Open centered Modal</Button>
</>
);
}
헤더 제거 (Remove header)
헤더를 제거하려면 withCloseButton={false}로 설정해요.
import { useDisclosure } from '@mantine/hooks';
import { Modal, Button } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Modal opened={opened} onClose={close} withCloseButton={false}>
Modal without header, press escape or click on overlay to close
</Modal>
<Button onClick={open}>Open modal</Button>
</>
);
}
크기 변경 (Change size)
size prop을 사전 정의된 크기나 유효한 너비(예: 55%, 50rem)로 설정해 모달 너비를 변경할 수 있어요. Modal의 너비는 100vw를 초과할 수 없어요. 크기 값: xs, sm, md, lg, xl, 55rem, 70%, 100%.
크기 auto (Size auto)
size="auto"인 Modal은 콘텐츠에 맞는 너비를 가져요.
import { useDisclosure, useCounter } from '@mantine/hooks';
import { Modal, Button, Group, Text, Badge } from '@mantine/core';
function Demo() {
const [opened, { close, open }] = useDisclosure(false);
const [count, { increment, decrement }] = useCounter(3, { min: 0 });
const badges = Array(count)
.fill(0)
.map((_, index) => <Badge key={index}>Badge {index}</Badge>);
return (
<>
<Modal opened={opened} onClose={close} size="auto">
<Text>Modal with size auto will fit its content</Text>
<Group>{badges}</Group>
<Group mt="md">
<Button onClick={increment} variant="default">Add badge</Button>
<Button onClick={decrement} variant="default">Remove badge</Button>
</Group>
</Modal>
<Button onClick={open}>Open modal</Button>
</>
);
}
전체 화면 (Fullscreen)
전체 화면 모달은 화면 전체를 차지해요. fullScreen prop이 설정되면 보통 전환을 fade로 바꾸는 것이 좋아요.
import { useDisclosure } from '@mantine/hooks';
import { Modal, Button } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Modal opened={opened} onClose={close} fullScreen transitionProps={{ transition: 'fade' }}>
{/* Modal content */}
</Modal>
<Button onClick={open}>Open modal</Button>
</>
);
}
작은 화면의 기기에서만 Modal을 전체 화면으로 전환하려면 use-media-query 훅을 사용해요. fullScreen prop이 설정되면 size prop은 무시돼요.
import { useDisclosure, useMediaQuery } from '@mantine/hooks';
import { Modal, Button } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
const isMobile = useMediaQuery('(max-width: 50em)');
return (
<>
<Modal opened={opened} onClose={close} fullScreen={isMobile} title="Fullscreen on mobile">
The Modal will be full screen only on mobile
</Modal>
<Button onClick={open}>Open modal</Button>
</>
);
}
오버레이 커스터마이즈 (Customize overlay)
Modal은 Overlay 컴포넌트를 사용해요. overlayProps로 Overlay가 지원하는 어떤 prop이든 설정할 수 있어요.
import { useDisclosure } from '@mantine/hooks';
import { Modal, Button } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Modal opened={opened} onClose={close} overlayProps={{ blur: 3 }}>
{/* Modal content */}
</Modal>
<Button onClick={open}>Open modal</Button>
</>
);
}
스크롤이 있는 Modal (Modal with scroll)
scrollAreaComponent나 콘텐츠를 넘치는 많은 요소를 넣으면 모달 본문이 스크롤돼요.
import { useDisclosure } from '@mantine/hooks';
import { Modal, Button } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
const content = Array(100)
.fill(0)
.map((_, index) => <p key={index}>Modal with scroll</p>);
return (
<>
<Modal opened={opened} onClose={close} title="Title">
{content}
</Modal>
<Button onClick={open}>Open modal</Button>
</>
);
}
ScrollArea와 함께 (Usage with ScrollArea)
scrollAreaComponent={ScrollArea.Autosize}로 ScrollArea를 사용할 수 있어요.
import { useDisclosure } from '@mantine/hooks';
import { Modal, Button, ScrollArea } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
const content = Array(100)
.fill(0)
.map((_, index) => <p key={index}>Modal with scroll</p>);
return (
<>
<Modal opened={opened} onClose={close} title="Title" scrollAreaComponent={ScrollArea.Autosize}>
{content}
</Modal>
<Button onClick={open}>Open modal</Button>
</>
);
}
오프셋 변경 (Change offsets)
xOffset/yOffset으로 가로/세로 콘텐츠 오프셋을 설정할 수 있어요.
import { useDisclosure } from '@mantine/hooks';
import { Modal, Button } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Modal opened={opened} onClose={close} xOffset="5%" yOffset="5%">
{/* Modal content */}
</Modal>
<Button onClick={open}>Open modal</Button>
</>
);
}
전환 변경 (Change transitions)
Modal은 Transition 컴포넌트로 만들어져요. transitionProps prop으로 Transition 속성을 커스터마이즈할 수 있어요.
import { useState } from 'react';
import { Modal, Group, Button } from '@mantine/core';
function Demo() {
const [noTransitionOpened, setNoTransitionOpened] = useState(false);
const [slowTransitionOpened, setSlowTransitionOpened] = useState(false);
return (
<>
<Modal
opened={slowTransitionOpened}
onClose={() => setSlowTransitionOpened(false)}
title="Please consider this"
transitionProps={{ transition: 'rotate-left' }}
>
rotate-left transition
</Modal>
<Modal
opened={noTransitionOpened}
onClose={() => setNoTransitionOpened(false)}
title="Please consider this"
transitionProps={{ transition: 'fade', duration: 600, timingFunction: 'linear' }}
>
fade transition 600ms linear transition
</Modal>
<Group>
<Button onClick={() => setSlowTransitionOpened(true)} variant="default">Rotate left transition</Button>
<Button onClick={() => setNoTransitionOpened(true)} variant="default">Fade transition</Button>
</Group>
</>
);
}
onExitTransitionEnd와 onEnterTransitionEnd
onExitTransitionEnd와 onEnterTransitionEnd prop은 닫힘/열림 전환이 끝난 후 코드를 실행하는 데 사용할 수 있어요. 예를 들어 모달이 닫힌 후 데이터를 지우고 싶을 때 유용해요.
import { useState } from 'react';
import { Button, Group, Modal } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
function Demo() {
const [firstOpened, firstHandlers] = useDisclosure(false);
const [secondOpened, secondHandlers] = useDisclosure(false);
const [modalData, setModalData] = useState({ title: '', message: '' });
return (
<>
<Modal
opened={firstOpened}
onClose={() => {
firstHandlers.close();
setModalData({ title: '', message: '' });
}}
title={modalData.title}
>
{modalData.message}
</Modal>
<Modal
opened={secondOpened}
onClose={secondHandlers.close}
onExitTransitionEnd={() => setModalData({ title: '', message: '' })}
title={modalData.title}
>
{modalData.message}
</Modal>
<Group>
<Button
onClick={() => {
firstHandlers.open();
setModalData({ title: 'Edit your profile', message: 'Imagine a form here' });
}}
>
Clear data in onClose
</Button>
<Button
onClick={() => {
secondHandlers.open();
setModalData({ title: 'Edit your profile', message: 'Imagine a form here' });
}}
>
Clear data in onExitTransitionEnd
</Button>
</Group>
</>
);
}
초기 포커스 (Initial focus)
Modal은 FocusTrap으로 포커스를 가둬요. 초기 포커스를 받을 요소에 data-autofocus 속성을 추가해요.
import { useDisclosure } from '@mantine/hooks';
import { Modal, Button, TextInput } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Modal opened={opened} onClose={close}>
<TextInput data-autofocus label="Name" placeholder="Your name" />
</Modal>
<Button onClick={open}>Open modal</Button>
</>
);
}
모달이 열릴 때 어떤 요소에도 포커스하고 싶지 않다면 FocusTrap.InitialFocus 컴포넌트로 초기 포커스를 받는 시각적으로 숨겨진 요소를 만들 수 있어요.
data-autofocus 속성을 추가하지 않고 FocusTrap.InitialFocus도 사용하지 않으면, 모달은 내부의 첫 번째 포커스 가능한 요소(보통 닫기 버튼)에 포커스해요.
동작 제어 (Control behavior)
다음 prop으로 Modal 동작을 제어할 수 있어요. 대부분의 경우 이 기능을 끄는 것은 권장하지 않아요. 컴포넌트가 덜 접근 가능해져요.
trapFocus– 포커스를 모달 내부에 가둘지 결정closeOnEscape–Escape키를 누르면 모달을 닫을지 결정closeOnClickOutside– 사용자가 오버레이를 클릭하면 모달을 닫을지 결정returnFocus– 모달이 열리기 전에 포커스되어 있던 요소로 포커스를 돌려줄지 결정
react-remove-scroll 설정
Modal은 react-remove-scroll 패키지로 스크롤을 잠가요. removeScrollProps로 RemoveScroll 컴포넌트에 prop을 전달할 수 있어요.
import { Modal } from '@mantine/core';
function Demo() {
return <Modal removeScrollProps={{ allowPinchZoom: true }} opened onClose={() => {}}>{/* ... */}</Modal>;
}
닫기 아이콘 변경 (Change close icon)
closeButtonProps로 닫기 버튼을 커스터마이즈할 수 있어요.
import { XCircleIcon } from '@phosphor-icons/react';
import { useDisclosure } from '@mantine/hooks';
import { Modal, Button } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Modal opened={opened} onClose={close} closeButtonProps={{ icon: <XCircleIcon size={20} /> }}>
{/* Modal content */}
</Modal>
<Button onClick={open}>Open modal</Button>
</>
);
}
복합 컴포넌트 (Compound components)
Modal 렌더링을 완전히 제어하려면 다음 복합 컴포넌트를 사용할 수 있어요.
Modal.Root– 컨텍스트 제공자Modal.Overlay– Overlay 렌더링Modal.Content– 메인 모달 요소, 모든 모달 콘텐츠를 포함해야 해요Modal.Header– 고정(sticky) 헤더, 보통Modal.Title과Modal.CloseButton을 포함Modal.Title–h2요소,Modal.Content의aria-labelledby가 이 요소를 가리켜요, 보통Modal.Header안에 렌더링Modal.CloseButton– 닫기 버튼, 보통Modal.Header안에 렌더링Modal.Body– 메인 콘텐츠를 위한 자리,Modal.Content의aria-describedby가 이 요소를 가리켜요
import { useDisclosure } from '@mantine/hooks';
import { Modal, Button } from '@mantine/core';
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Modal.Root opened={opened} onClose={close}>
<Modal.Overlay />
<Modal.Content>
<Modal.Header>
<Modal.Title>Modal title</Modal.Title>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body>
Modal content
</Modal.Body>
</Modal.Content>
</Modal.Root>
<Button onClick={open}>Open modal</Button>
</>
);
}
Modal.Stack
Modal.Stack 컴포넌트로 여러 모달을 동시에 렌더링할 수 있어요. Modal.Stack은 열린 모달을 추적하고 z-index 값, 포커스 트래핑, closeOnEscape 동작을 관리해요. Modal.Stack은 useModalsStack 훅과 함께 사용하도록 설계돼요.
여러 Modal 컴포넌트를 사용하는 것과의 차이:
Modal.Stack은 z-index 값을 관리해요 – 나중에 열린 모달은 DOM 순서와 무관하게 항상 더 높은 z-index 값을 가져요Modal.Stack은 현재 열린 모달을 제외한 모든 모달의 포커스 트랩과Escape키 처리를 비활성화해요- 현재 열려 있지 않은 모달은 DOM에 존재하지만
opacity: 0과pointer-events: none으로 숨겨져요 - 한 번에 하나의 오버레이만 렌더링돼요
import { Button, Group, Modal, useModalsStack } from '@mantine/core';
function Demo() {
const stack = useModalsStack(['delete-page', 'confirm-action', 'really-confirm-action']);
return (
<>
<Modal.Stack>
<Modal
opened={stack.state['delete-page']}
onClose={stack.close('delete-page')}
title="Delete this page?"
>
Are you sure you want to delete this page? This action cannot be undone.
<Group justify="flex-end" mt="md">
<Button variant="default" onClick={stack.close('delete-page')}>Cancel</Button>
<Button color="red" onClick={() => stack.open('confirm-action')}>Delete</Button>
</Group>
</Modal>
<Modal
opened={stack.state['confirm-action']}
onClose={stack.close('confirm-action')}
title="Are you sure?"
>
Are you sure you want to perform this action? This action cannot be undone. If you are
sure, press confirm button below.
<Group justify="flex-end" mt="md">
<Button variant="default" onClick={stack.close('confirm-action')}>Cancel</Button>
<Button color="red" onClick={() => stack.open('really-confirm-action')}>Confirm</Button>
</Group>
</Modal>
<Modal
opened={stack.state['really-confirm-action']}
onClose={stack.close('really-confirm-action')}
title="Are you really sure?"
>
Jokes aside. You have confirmed this action. This is your last chance to cancel it. After
you press confirm button below, action will be performed and cannot be undone. For real
this time. Are you sure you want to proceed?
<Group justify="flex-end" mt="md">
<Button variant="default" onClick={stack.close('really-confirm-action')}>Cancel</Button>
<Button color="red">Confirm</Button>
</Group>
</Modal>
</Modal.Stack>
<Button onClick={() => stack.open('delete-page')}>Open modal</Button>
</>
);
}
Modal.Stack은 Modal 컴포넌트와만 사용할 수 있어요. Modal.Root와 다른 복합 컴포넌트로 만든 컴포넌트는 Modal.Stack과 호환되지 않아요.
useModalsStack 훅
useModalsStack 훅은 여러 모달을 동시에 쉽게 제어하는 방법을 제공해요. 고유한 모달 ID 배열을 받고 다음 속성을 가진 객체를 반환해요.
interface UseModalsStackReturnType<T extends string> {
// Current opened state of each modal
state: Record<T, boolean>;
// Opens modal with the given id
open: (id: T) => void;
// Closes modal with the given id
close: (id: T) => void;
// Toggles modal with the given id
toggle: (id: T) => void;
// Closes all modals within the stack
closeAll: () => void;
// Returns props for modal with the given id
register: (id: T) => {
opened: boolean;
onClose: () => void;
stackId: T;
};
}
Modal 컴포넌트와 useModalsStack을 사용하는 예시:
import { Modal, useModalsStack } from '@mantine/core';
function Demo() {
const stack = useModalsStack(['first', 'second']);
return (
<>
<Modal {...stack.register('first')}>First</Modal>
<Modal {...stack.register('second')}>Second</Modal>
<Button onClick={() => stack.open('first')}>Open first</Button>
</>
);
}
고정 요소 오프셋 (Fixed elements offset)
Modal 컴포넌트는 react-remove-scroll 패키지로 스크롤을 잠가요. 이러한 요소를 올바르게 크기 조절하려면 className을 추가해요 (문서).
접근성 (Accessibility)
Modal 컴포넌트는 접근성에 대해 WAI-ARIA 권장사항을 따릅니다.
title prop을 설정하면 컴포넌트를 접근 가능하게 만들고, 콘텐츠 요소에 aria-labelledby가 추가돼요.
import { Modal } from '@mantine/core';
function Demo() {
return <Modal opened onClose={() => {}} title="Modal title">{/* ... */}</Modal>;
}
닫기 버튼의 aria-label을 설정하려면 closeButtonProps를 사용해요.
import { Modal } from '@mantine/core';
function Demo() {
return <Modal opened onClose={() => {}} closeButtonProps={{ 'aria-label': 'Close modal' }}>{/* ... */}</Modal>;
}