대화상자

개요

모달 및 비모달 대화상자에 네이티브 <dialog> 요소를 사용합니다. 접근성 시맨틱스, 포커스 처리, 최상위 레이어 렌더링, Esc 처리를 제공하므로 맞춤 오버레이를 직접 다시 빌드할 필요가 없습니다.

오버레이 유형 선택

일치하는 첫 번째 규칙을 선택합니다. 확실하지 않은 경우 규칙 1을 사용합니다.

규칙 1: 오버레이가 사용자를 방해하고 페이지를 차단함 (기본값)

확인, 양식, 로그인, 파괴적 작업, 별도로 명시되지 않은 '모달', '대화상자', '라이트박스' 또는 '팝업' 요청에 사용합니다.

<dialog>를 모달로 열고, command="show-modal"를 사용하여 선언적으로 사용하거나(선언적 대화상자 및 팝오버 컨트롤 참고) showModal()를 사용하여 스크립트에서 사용합니다.

<button type="button" command="show-modal" commandfor="confirm">Delete</button>

<!-- MANDATORY: no role="dialog" or aria-modal; the native modal already provides them -->
<dialog id="confirm" aria-labelledby="confirm-title">
  <h2 id="confirm-title">Delete this item?</h2>
  <form method="dialog">
    <button value="cancel" autofocus>Cancel</button>
    <button value="delete">Delete</button>
  </form>
</dialog>

브라우저는 비활성 페이지, 포커스 포함, 최상위 레이어, ::backdrop, Esc 및 뒤로 동작 닫기, 호출자로 포커스 반환을 제공합니다.

  • role="dialog", aria-modal, 포커스 트랩, inert 전환 또는 Esc 핸들러를 추가하지 마세요.
  • show() 또는 open 속성으로 열지 마세요.
  • 아래 CSS를 사용하여 페이지 스크롤을 중지합니다. 자세한 내용은 모달 대화상자 아래의 페이지가 스크롤되지 않도록 방지를 참고하세요.
  • html 또는 body에서 overflow: hidden 또는 position: fixed를 사용하여 스크롤을 잠그지 마세요.
/* MANDATORY (both rules): without them, scrolling over the backdrop or inside the dialog scrolls the page behind it */
dialog:modal {
  overscroll-behavior: contain; /* stops scroll gestures in the dialog chaining to the page */
}
dialog::backdrop {
  overflow: hidden; /* ::backdrop is not a scroll container by default, so it would ignore overscroll-behavior */
  overscroll-behavior: contain;
}

메뉴 또는 도움말에는 적용되지 않음: 규칙 3을 참고하세요.

규칙 2: 패널이 열려 있는 동안 사용자가 페이지에서 계속 작업함

의미상 여전히 대화상자인 검색창, 검사기, 팔레트, 작성 창에 사용합니다.

팝오버 닫기가 있는 최상위 레이어 패널에는 <dialog popover>를 사용합니다 (auto는 가볍게 닫고 manual는 그렇지 않음). 패널이 최상위 레이어에 있으면 안 되는 경우에만 dialog.show()를 사용합니다.

<button type="button" popovertarget="find">Find</button>
<dialog id="find" popover="manual" aria-labelledby="find-title">
  <h2 id="find-title">Find</h2>
  <input type="search" aria-label="Search">
  <button type="button" popovertarget="find" popovertargetaction="hide">Close</button>
</dialog>

페이지는 대화형 상태로 유지되며 비활성 상태가 되지 않습니다.

  • 권장사항: 닫기 버튼을 표시합니다.
  • 페이지를 차단하는 showModal()를 사용하지 마세요.

확인 차단용이 아님: 규칙 1을 참고하세요.

규칙 3: 오버레이는 대화상자가 아님

메뉴, 드롭다운, 도움말, 마우스 오버 카드, 토스트 메시지에 사용합니다.

<dialog>가 아닌 popover 속성이 있는 시맨틱 요소를 사용합니다.

<button type="button" popovertarget="menu">Actions</button>
<ul id="menu" popover>
  <li><a href="/rename">Rename</a></li>
</ul>

해제 방법 선택

기본값은 대화상자가 열리는 방식에 따라 다릅니다. closedby가 없거나 유효하지 않은 경우 showModal()는 closerequest로 작동하고 다른 열기 방법은 none로 작동합니다.

  • Esc 및 뒤로 동작만 (모달 기본값): 아무것도 설정하지 않거나 closedby="closerequest"로 설정합니다.
  • 외부를 클릭하여 닫기: closedby="any"를 사용합니다.
  • 개발자만 닫을 수 있는 경우 (예: 저장되지 않은 변경사항이 있는 양식): closedby="none"를 사용하고 확인 경로를 추가합니다. 아래의 '마무리'를 참고하세요.

가이드라인

액세스 가능한 이름 및 초기 포커스

  • aria-labelledby가 제목을 가리키도록 하거나 표시되는 제목이 없는 경우 aria-label를 사용하여 모든 대화상자의 이름을 지정해야 합니다(DO).
  • 권장사항: 기본 컨트롤 또는 첫 번째 필드에 autofocus를 배치합니다.
  • 파괴적인 확인에서 안전한 작업 (예: 취소 또는 유지)에 autofocus를 배치합니다.
  • 닫을 때 포커스를 반환하는 코드를 작성하지 마세요. 브라우저가 이를 호출자에게 반환합니다.

서론

마무리

  • <form method="dialog"> 버튼 또는 command="close"를 사용하여 닫습니다(권장).
  • 권장사항: closedby를 신중하게 선택합니다. any (라이트 닫기 및 Esc), closerequest (Esc만, 모달 기본값) 또는 none (개발자 제어 닫기만)
  • 권장사항: 저장되지 않은 변경사항 로직을 준수해야 하는 취소 버튼에는 command="request-close" 또는 requestClose()를 사용합니다. cancel를 먼저 실행하고 closedby를 무시합니다.
  • closedby를 점진적 개선으로 취급합니다. Esc 및 모바일 뒤로 동작은 이 기능 없이도 이미 모달을 닫습니다.

종료에 후킹

  • close 리스너에서 dialog.returnValue를 읽어 어떤 버튼이 사용되었는지 확인한 다음 dialog.returnValue = ''로 재설정해야 합니다. Esc 및 기타 종료 요청은 값을 설정하지 않으며 브라우저마다 이전 값을 삭제하는지 여부가 다르므로 재설정하지 않으면 오래된 값이 유출될 수 있습니다. ''을 '선택하지 않고 닫음'으로 처리합니다.
  • cancel를 사용하여 Esc 및 request-close를 가로채야 하지만(DO) 사용자 활성화 없이 반복되는 Esc 누르기에 preventDefault()를 사용해서는 안 됩니다.
  • 더티 양식에 대한 명시적 확인 단계와 함께 closedby="none"를 사용합니다.
  • cancel가 차단될 때 대화상자가 열려 있는 이유를 사용자에게 알려야 합니다(DO). 표시되는 메시지를 표시하고 포커스를 받는 컨트롤(예: 삭제)에서 aria-describedby로 참조하여 포커스가 이동할 때 스크린 리더가 이를 읽도록 합니다. 이 속성이 없으면 포커스가 새 버튼으로 이동할 때 해당 버튼의 이름만 알려줍니다.
  • 금지사항: 확인으로 인해 컨트롤이 포인터 아래로 이동하게 하지 마세요. 메시지 공간을 예약하고(display 또는 hidden가 아닌 visibility로 숨김) 기존 버튼이 이동하지 않는 위치 (예: 오른쪽 정렬 행의 첫 번째)에 소멸 버튼을 추가하여 같은 지점을 두 번 클릭해도 사용자 변경사항이 삭제되지 않도록 합니다.

비모달 대화상자

  • 최상위 레이어 패널에는 <dialog popover>(auto 또는 manual)를 사용하고 최상위 레이어를 원하지 않는 경우에는 dialog.show()를 사용합니다(DO).
  • 모든 비모달 대화상자에 표시되는 닫기 버튼을 포함합니다.

배경 및 스타일링

접근성

  • role="dialog", aria-modal, 수동 포커스 트랩 또는 inert 전환을 네이티브 모달에 추가하지 마세요.
  • 닫기 컨트롤에 액세스 가능한 이름이 있는 네이티브 <button>를 사용합니다.
  • 대화상자 제목에는 일관된 제목 수준을 하나만 사용합니다.

대체 전략

closedby(dialog-closedby)

closedby가 없어도 모달은 Esc 및 뒤로 동작으로 닫힙니다. 외부 클릭만 손실되므로 이는 단계적 성능 저하입니다. closedby="none"도 같은 방식으로 무시됩니다. Esc는 cancel를 실행한 후 대화상자를 닫습니다. 저장되지 않은 변경사항 가드를 closedby="none" 단독이 아닌 cancel 핸들러에 유지하여 closedby가 누락된 경우에도 보호합니다. 기준 타겟에 가벼운 닫기가 필요한 경우 closedby가 누락된 위치에 다음을 추가합니다.

if (!('closedBy' in HTMLDialogElement.prototype)) {
  // Test the pointer position, not event.target: the <dialog> is also the target for its own
  // padding and the gaps between its children, so a target check would close on those clicks
  const outside = (e, dialog) => {
    const r = dialog.getBoundingClientRect();
    return e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;
  };
  for (const dialog of document.querySelectorAll('dialog[closedby="any"]')) {
    let pressedOutside = false;
    dialog.addEventListener('pointerdown', (e) => { pressedOutside = outside(e, dialog); });
    // Require the press and the release outside, so a text-selection drag that ends on the backdrop does not dismiss
    dialog.addEventListener('pointerup', (e) => { if (pressedOutside && outside(e, dialog)) dialog.close(); });
  }
}

overscroll-behavior

스크롤 포함을 점진적 개선으로 취급합니다. 기능을 감지하지 마세요. @supports는 스크롤할 수 없는 컨테이너가 속성을 무시하는 경우에도 속성을 지원되는 것으로 보고합니다. 이 속성이 적용되지 않는 경우에도 대화상자에는 자체 콘텐츠가 오버플로될 때마다 스크롤이 포함되며 showModal()는 여전히 페이지를 비활성 상태로 만듭니다. html 또는 body에서 overflow: hidden 또는 position: fixed로 대체하지 마세요. 루트 스크롤바가 삭제되고 레이아웃이 변경됩니다.

호출자 명령어 (command/commandfor)

기준 타겟에 호출자 명령어가 포함되지 않은 경우 동일한 마크업을 유지하고 폴리필을 조건부로 로드합니다 (점진적 개선과 동일한 환경). 인터페이스에서 감지하고 무조건 로드하지 않습니다.

// MANDATORY: feature detect on HTMLButtonElement.prototype and load only when native support is missing
if (!('commandForElement' in HTMLButtonElement.prototype)) {
  import('invokers-polyfill'); // npm install invokers-polyfill
}

규칙 2와 3만 팝오버를 사용합니다.

Popover의 대체 및 브라우저 지원

Popover API는 대부분 점진적 개선이지만, 최상위 레이어 승격, 가벼운 닫기, popovertarget 호출과 같은 정의 동작에는 CSS 전용에 상응하는 기능이 없습니다. 이전 브라우저에는 폴리필이 필요하며, 폴리필을 제공하지 않으려면 수동 대체가 필요합니다.

폴리필: 이전 브라우저에서 popover 속성을 지원하려면 조건부로 @oddbird/popover-polyfill를 로드합니다. 필수: HTMLElement.prototype에서 popover 속성을 확인하여 기능을 감지하고 네이티브 지원이 누락된 경우에만 폴리필을 로드합니다. 무조건 로드하지 마세요.

번들러 또는 가져오기 맵 사용:

// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
  import("@oddbird/popover-polyfill");
}

번들러가 없는 경우 <script type="module"> 내의 CDN에서 가져옵니다.

<script type="module">
  if (!("popover" in HTMLElement.prototype)) {
    import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
  }
</script>

스타일 지정 주의사항: 폴리필은 실제 :popover-open 의사 클래스를 정의할 수 없으므로 대신 .\:popover-open 클래스를 적용합니다. 필수: :is() 또는 :where()를 사용하여 두 가지를 결합합니다. 그렇지 않으면 :popover-open가 없는 브라우저에서 전체 규칙을 삭제합니다.

[popover]:is(:popover-open, .\:popover-open) {
  display: block;
}

또는 폴리필이 없는 기존 대체 기능의 경우 position: fixed를 사용하고 getBoundingClientRect()를 통해 좌표를 수동으로 계산하거나, 사용 사례에 적합한 경우 inset: auto를 사용하여 기본 위치 지정에 의존합니다.