Modal

Modal은 메인 콘텐츠 위에 오버레이로 표시되는 다이얼로그 컴포넌트입니다. 일반 Modal과 사용자의 명시적 확인이 필요한 AlertModal 두 가지 타입을 제공합니다.

2
Types
6
Sizes
2
Scroll Mode
Radix
Base

Playground

Preview
SMModal
Modal
Type
Size
Scroll
Content
Options
Button Style
Cancel
Confirm
const [open, setOpen] = useState(false)

<Modal open={open} onOpenChange={setOpen}>
  <Modal.Trigger asChild>
    <Button variant="outline">Open Modal</Button>
  </Modal.Trigger>
  <Modal.Content>
    <Modal.Header>
      <Modal.Title>Modal Title</Modal.Title>
      <Modal.Description>This is a description of the modal.</Modal.Description>
    </Modal.Header>
    <Modal.Body>
      <p>This is a modal dialog. You can place any content here, including forms, confirmations, or informational messages.</p>
    </Modal.Body>
    <Modal.Footer>
      <Modal.Close asChild>
        <Button variant="ghost" size="md" pressEffect={false}>Cancel</Button>
      </Modal.Close>
      <Button size="md" pressEffect={false}>Confirm</Button>
    </Modal.Footer>
  </Modal.Content>
</Modal>

Sizes

xs간단한 확인 또는 알림
max-width360px
sm (default)표준 (기본값)
max-width480px
md표준 폼
max-width600px
lg복잡한 폼
max-width760px
xl테이블 또는 대시보드
max-width960px
full전체 너비
max-widthFull
패딩 (전 사이즈 공통)
Header24px · bottom 0
Body24px
Footer24px · top 0
Closeright 16px, top 16px

Features

Scroll Behavior

"outside": 모달 전체가 스크롤됩니다. "inside": Body만 스크롤되고 Header와 Footer는 고정됩니다.

{/* Outside scroll — entire modal scrolls within viewport */}
<Modal.Content scrollBehavior="outside">
  <Modal.Header>
    <Modal.Title>Outside Scroll</Modal.Title>
    <Modal.Description>The entire modal scrolls within the viewport.</Modal.Description>
  </Modal.Header>
  <Modal.Body>
    <p>Long content here...</p>
  </Modal.Body>
</Modal.Content>

{/* Inside scroll — only body scrolls; header stays fixed */}
<Modal.Content scrollBehavior="inside">
  <Modal.Header>
    <Modal.Title>Inside Scroll</Modal.Title>
    <Modal.Description>Only the body scrolls; header and footer stay fixed.</Modal.Description>
  </Modal.Header>
  <Modal.Body>
    <p>Long content here...</p>
  </Modal.Body>
</Modal.Content>

Sidebar Layout

lg / xl / full 사이즈 전용. Modal.Body를 p-0 flex로 분할해 좌측 내비게이션과 우측 콘텐츠 레이아웃을 구성할 수 있습니다.

const [active, setActive] = useState(0)
const navItems = ['General', 'Security', 'Notifications']

<Modal.Content size="lg" scrollBehavior="inside" showCloseButton={false}>
  <Modal.Body className="p-0 flex min-h-0">
    {/* Left sidebar */}
    <div className="w-48 shrink-0 flex flex-col">
      <div className="p-3">
        <Modal.Close asChild>
          <Button variant="ghost" size="md" pressEffect={false} className="w-9 h-9 p-0">
            <X className="icon-sm" />
            <span className="sr-only">Close</span>
          </Button>
        </Modal.Close>
      </div>
      <nav className="flex-1 px-2 pb-4 space-y-1">
        {navItems.map((item, i) => (
          <button key={item} onClick={() => setActive(i)}
            className={cn("w-full text-left px-3 py-2 text-md rounded-lg transition-colors",
              active === i
                ? "bg-background-muted text-foreground font-semibold"
                : "text-text-muted hover:text-foreground hover:bg-background-muted"
            )}>
            {item}
          </button>
        ))}
      </nav>
    </div>
    <Divider orientation="vertical" color="muted" className="mx-0 h-auto" />
    {/* Right content */}
    <div className="flex-1 flex flex-col min-w-0 overflow-hidden">
      <div className="px-6 pt-6 pb-4 shrink-0">
        <h2 className="text-lg font-semibold">{navItems[active]}</h2>
      </div>
      <div className="flex-1 px-6 pb-6 overflow-y-auto">
        <p className="text-md text-text-muted">Content area...</p>
      </div>
    </div>
  </Modal.Body>
</Modal.Content>

Close Button

false로 설정하면 닫기 버튼이 숨겨집니다. closeIcon prop으로 아이콘을 교체할 수 있습니다.

{/* With close button (default) — X only */}
<Modal.Content showCloseButton>
  <Modal.Header>...</Modal.Header>
  <Modal.Body>...</Modal.Body>
</Modal.Content>

{/* Without close button — footer actions provide exit */}
<Modal.Content showCloseButton={false}>
  <Modal.Header>...</Modal.Header>
  <Modal.Body>...</Modal.Body>
  <Modal.Footer>
    <Modal.Close asChild>
      <Button variant="ghost" size="md" pressEffect={false}>Cancel</Button>
    </Modal.Close>
    <Button size="md" pressEffect={false}>Confirm</Button>
  </Modal.Footer>
</Modal.Content>

API

Component Structure

ModalRadix Dialog
.Trigger.ContentProps.Header.Title.Description.Body.Footer.CloseProps.Overlay.Portal
AlertModalRadix AlertDialog
.Trigger.ContentProps.Header.Title.Description.Body.Footer.Action.Cancel.Overlay.Portal

Props

Modal.Content

size"sm"
"xs" | "sm" | "md" | "lg" | "xl" | "full"

콘텐츠 최대 너비 (360/480/600/760/960/full)

scrollBehavior"outside"
"inside" | "outside"

콘텐츠 오버플로 시 스크롤 위치

showCloseButtontrue
boolean

내장 닫기 버튼 표시

closeIconBuilt-in SVG icon
ReactNode

커스텀 닫기 아이콘

AlertModal.Content

size"sm"
"xs" | "sm"

콘텐츠 최대 너비 (360/480)

Modal.Close

asChildfalse
boolean

자식 요소를 트리거로 사용

Anatomy

1
2
3
4
Modal title
5
Modal description
6
7
8
1
Overlay
배경 오버레이
2
Content
다이얼로그 패널
3
Header
타이틀 영역
4
Title
제목 텍스트
5
Description
보조 설명 텍스트
6
Close
닫기 버튼
7
Body
메인 콘텐츠
8
Footer
액션 버튼

Best Practices

권장

  • 파괴적 작업에는 AlertModal을 사용하세요
  • 모달에 항상 타이틀과 설명을 설정하세요
  • 긴 콘텐츠에는 scrollBehavior="inside"를 사용하세요
  • 콘텐츠에 적합한 사이즈를 선택하세요

지양

  • 모달 안에 모달을 중첩하지 마세요
  • 간단한 알림에는 모달 대신 Toast를 사용하세요
  • 닫을 수 없는 모달을 만들지 마세요
  • 확인이 필요한 경우에만 AlertModal을 사용하세요

Accessibility

키보드 조작

Escape 키로 모달을 닫을 수 있습니다. AlertModal은 오버레이 클릭으로 닫히지 않습니다. Tab 키는 모달 내부에 포커스를 가두어 외부 요소로 이동하지 않습니다.

ARIA / WCAG

  • role="dialog" Radix Dialog가 자동 설정
  • role="alertdialog" AlertModal이 자동 설정
  • aria-labelledby Modal.Title에 자동 연결
  • aria-describedby Modal.Description에 자동 연결
  • 포커스 트랩: 모달 내부에 포커스를 제한
  • 닫기 버튼에 sr-only 레이블 설정