3P by GN⁺ | ★ favorite | 댓글 1개
  • HTML <dialog> 요소는 브라우저 네이티브 모달·비모달 대화상자를 만들며, 모달은 페이지 나머지 영역을 inert 상태로 만들어 상호작용을 차단함
  • 열고 닫는 방식은 showModal(), show(), close()가 기본이며, <form method="dialog">, Invoker Commands API, Popover API로도 선언형 제어가 가능함
  • closedby 속성은 사용자가 대화상자를 닫을 수 있는 경로를 any, closerequest, none으로 나누고, 열린 방식에 따라 기본 동작이 달라짐
  • 접근성에서는 초기 포커스와 명시적 닫기 버튼이 중요하며, showModal()로 열린 대화상자는 기본적으로 첫 번째 포커스 가능 요소와 Esc 키 닫기를 제공함
  • 스타일링은 :modal, :open, ::backdrop을 활용하고, 애니메이션에는 display, overlay, @starting-style, transition-behavior: allow-discrete 같은 이산 전환 처리가 필요함

<dialog>의 역할과 모달 동작

  • HTML <dialog> 요소는 모달 및 비모달 대화상자를 만들기 위한 요소임
  • 모달 대화상자는 다른 UI 요소와의 상호작용을 막고, 페이지의 나머지 부분을 inert 상태로 만듦
  • 비모달 대화상자는 열린 상태에서도 페이지의 나머지 부분과 계속 상호작용할 수 있음

열림 상태와 닫기 정책

  • <dialog>는 전역 속성을 포함하지만, tabindex 속성은 사용하면 안 됨
    • <dialog> 자체는 상호작용 요소가 아니며 포커스를 받지 않음
    • 내부 콘텐츠와 닫기 버튼은 포커스를 받고 상호작용할 수 있음
  • open 속성은 대화상자가 활성 상태이며 상호작용 가능함을 나타냄
    • open 속성이 없으면 사용자에게 보이지 않음
    • 대화상자를 표시할 때는 open 속성보다 .show() 또는 .showModal() 사용이 권장됨
    • open 속성으로 열린 <dialog>는 비모달임
    • 비모달 대화상자의 열림·닫힘 상태를 open 속성 토글로 바꿀 수는 있지만 권장되지 않음
  • closedby 속성은 사용자가 어떤 방식으로 대화상자를 닫을 수 있는지 지정함
    • any: 세 가지 방식 모두로 닫을 수 있음
      • 대화상자 바깥 클릭 또는 탭 같은 light dismiss
      • Esc 키, 모바일의 뒤로 가기 또는 dismiss 제스처 같은 플랫폼별 동작
      • HTMLDialogElement.close()를 호출하는 버튼이나 <form> 제출 같은 개발자 지정 방식
    • closerequest: 플랫폼별 동작과 개발자 지정 방식으로 닫을 수 있음
    • none: 개발자 지정 방식으로만 닫을 수 있음
    • 유효한 closedby 값이 없으면 showModal()로 열린 경우 closerequest처럼, 그 외에는 none처럼 동작함

JavaScript와 선언형 제어

  • JavaScript로 <dialog> 표시와 닫기를 직접 제어할 수 있음
    • showModal(): 모달 대화상자 표시
    • show(): 비모달 대화상자 표시
    • close(): 대화상자 닫기
    • <dialog> 안의 <form>method="dialog"로 제출해 닫을 수도 있음
    • 모달 대화상자는 Esc 키로도 닫을 수 있음
  • Invoker Commands API는 버튼 속성만으로 모달 대화상자를 열고 닫을 수 있게 함
    • <button>commandforcommand 속성을 지정함
    • 대화상자에 사용할 수 있는 명령은 "show-modal", "close", "request-close"
<button command="show-modal" commandfor="my-dialog">Open dialog</button>

<dialog id="my-dialog">
  <p>This dialog was opened using an invoker command.</p>
  <button commandfor="my-dialog" command="close">Close</button>
</dialog>
  • Popover API는 비모달 대화상자를 선언적으로 열고 닫고 토글할 수 있게 함
    • <dialog>popover 속성을 추가해 팝오버로 만듦
    • 버튼이나 입력 요소에 popovertarget, popovertargetaction을 지정함
    • 팝오버 대화상자는 비모달이므로 바깥을 클릭해 닫을 수 있음
    • popover 값을 지정하지 않으면 기본값 "auto"가 사용되어 light dismiss가 활성화됨
    • popover="manual"은 light dismiss를 비활성화하며, 닫기 버튼 같은 별도 방식이 필요함
    • popovertargetaction을 생략하면 기본값 toggle이 사용됨

폼 제출과 닫기 처리

  • 모든 <dialog>에는 닫기 메커니즘이 필요하며, 물리 키보드가 없는 기기에서도 동작해야 함
  • 닫기 방식은 여러 가지가 있음
    • <dialog> 내부 <form>method="dialog" 를 설정하고 제출
    • light dismiss가 활성화된 상태에서 대화상자 바깥 클릭
    • 허용된 대화상자에서 Esc 키 누르기
    • HTMLDialogElement.close() 호출
  • <form method="dialog"> 또는 제출 버튼의 formmethod="dialog"는 대화상자를 닫음
    • 폼 컨트롤 상태는 저장되지만 제출되지는 않음
    • returnValue는 활성화된 버튼의 값으로 설정됨
  • 필수 입력값이 있는 폼에서는 사용자 에이전트가 값이 제공될 때까지 일반 제출로 대화상자를 닫지 못하게 함
    • 닫기 버튼에 formnovalidate 를 사용하면 폼 검증을 우회할 수 있음
    • JavaScript에서 dialog.close()를 호출해도 닫을 수 있음

포커스와 접근성

  • showModal()<dialog>를 열면 기본적으로 첫 번째 중첩된 포커스 가능 요소에 포커스가 설정됨
  • 특정 대화상자에서 가장 적절한 초기 포커스 위치를 지정하려면 autofocus 속성을 사용할 수 있음
    • 즉시 상호작용할 요소가 없으면 닫기 버튼에 autofocus를 두는 것이 권장됨
    • 동적으로 렌더링되는 대화상자처럼 초기 포커스 위치가 불확실하면 <dialog> 자체가 적절한 초기 포커스 위치가 될 수 있음
  • 모든 사용자가 닫을 수 있도록 확인, 취소, 닫기 버튼 같은 명시적 버튼을 포함하는 방식이 가장 견고함
  • showModal()로 호출된 대화상자는 기본적으로 Esc 키로 닫힘
    • 비모달 대화상자는 기본적으로 Esc 키로 닫히지 않음
    • 키보드 사용자는 모달 대화상자가 Esc 키로 닫히기를 기대함
    • 여러 모달 대화상자가 열려 있으면 Esc 키는 마지막에 표시된 대화상자만 닫아야 하며, <dialog> 사용 시 브라우저가 이 동작을 제공함
  • 네이티브 <dialog>는 다른 요소로 만든 커스텀 대화상자에서 직접 복제해야 하는 사용성 및 접근성 기능을 제공함
  • 브라우저는 <dialog>를 ARIA role="dialog"를 사용하는 커스텀 대화상자와 유사하게 노출함
    • showModal()로 호출된 <dialog>는 암묵적으로 aria-modal="true"를 가짐
    • show(), open 속성, 기본 display 변경으로 표시된 <dialog>aria-modal="false"로 노출됨
    • 모달 대화상자 구현 시 <dialog>와 그 콘텐츠 외의 모든 것은 inert로 렌더링되어야 하며, showModal() 사용 시 브라우저가 이 동작을 제공함

CSS 스타일링과 backdrop

  • <dialog>는 일반 요소처럼 요소 이름으로 선택할 수 있음
  • 상태 기반 스타일링에는 :modal, :open 의사 클래스를 사용할 수 있음
  • 모달 대화상자의 배경은 ::backdrop 의사 요소로 스타일링할 수 있음
    • showModal()로 대화상자를 표시할 때 <dialog> 뒤에 나타나는 backdrop에 적용됨
    • inert 상태인 뒤쪽 콘텐츠를 흐리게 하거나 어둡게 하거나 가릴 수 있음

예제로 보는 사용 패턴

  • Invoker Commands API 예제는 command="show-modal" 버튼으로 대화상자를 열고, command="close" 버튼으로 닫음
    • “Open dialog” 버튼으로 열 수 있음
    • “Close” 버튼 또는 Esc 키로 닫을 수 있음
  • Popover API 예제는 popover, popovertarget, popovertargetaction으로 비모달 대화상자를 열고 닫음
    • “Close” 버튼, Esc 키, 대화상자 바깥 선택으로 닫을 수 있음
    • popover="manual"을 쓰면 light dismiss가 비활성화됨
  • open 속성 예제는 페이지 로드 시 이미 열린 HTML-only 비모달 대화상자를 만듦
    • <form method="dialog">의 “OK” 버튼으로 닫을 수 있음
    • 닫힌 뒤 다시 여는 방식은 제공되지 않음
    • 비모달 대화상자 표시는 HTMLDialogElement.show() 사용이 선호됨
  • 모달 대화상자 예제는 .showModal()로 열고 close()로 닫음
    • ::backdrop으로 그라데이션 배경을 스타일링함
    • 대화상자가 열리면 대화상자 외부는 inert 상태가 되어 문서와 상호작용할 수 없음
  • returnValue 예제는 폼과 버튼 값으로 대화상자의 반환값을 다룸
    • 기본 returnValue는 빈 문자열이거나, 대화상자 내부 폼을 제출한 버튼의 값임
    • “Confirm” 버튼은 선택값을 close()에 넘겨 반환값으로 사용함
    • Esc 키로 닫으면 returnValue가 갱신되지 않고 close 이벤트도 발생하지 않아 <output> 텍스트가 업데이트되지 않음

<dialog> 애니메이션

  • 숨겨진 <dialog>display: none, 표시된 <dialog>display: block이 됨
  • 표시 상태가 바뀔 때 top layer와 접근성 트리에 추가되거나 제거됨
  • <dialog>를 애니메이션하려면 display 속성이 애니메이션 가능해야 함
    • 지원 브라우저는 display를 이산 애니메이션 방식으로 처리함
    • none에서 block으로 바뀔 때는 0% 시점에 block으로 전환되어 전체 애니메이션 동안 보임
    • block에서 none으로 바뀔 때는 100% 시점에 none으로 전환되어 전체 애니메이션 동안 보임
  • CSS transition으로 애니메이션

    • CSS transition으로 <dialog>를 애니메이션하려면 다음 기능이 필요함
    • @starting-style: 대화상자가 열릴 때마다 전환 시작값 제공
    • display 전환: 전환 동안 대화상자를 보이는 display 값으로 유지
    • overlay 전환: top layer 제거를 전환 완료까지 지연
    • transition-behavior: allow-discrete: 기본적으로 애니메이션되지 않는 display, overlay의 이산 전환 활성화
    • :open 의사 클래스를 지원하지 않는 브라우저에서는 dialog[open] 속성 선택자로 열린 상태를 스타일링할 수 있음
    • <dialog>는 표시될 때마다 display: none에서 display: block으로 바뀌므로, 진입 전환마다 @starting-style에서 dialog:open 스타일로 전환됨
  • keyframe 애니메이션

    • CSS keyframe 애니메이션은 transition과 다른 제약을 가짐
    • @starting-style을 제공하지 않음
    • display 값을 keyframe 안에 포함함
    • allow-discrete를 명시적으로 활성화할 필요가 없음
    • overlay를 keyframe에 설정할 필요도 없음
    • backdrop fade-out은 대화상자가 닫힐 때 backdrop이 DOM에서 즉시 제거되어 애니메이션할 수 없음

기술 요약

  • 콘텐츠 카테고리: flow content, sectioning root
  • 허용 콘텐츠: flow content
  • 시작 태그와 종료 태그는 모두 필수이며 태그 생략은 없음
  • 허용 부모: flow content를 받는 모든 요소
  • 암묵적 ARIA 역할: dialog
  • 허용 ARIA 역할: alertdialog
  • DOM 인터페이스: HTMLDialogElement
  • 명세: HTML # the-dialog-element

댓글과 토론

Hacker News 의견들
  • 상호작용 가능한 HTML 요소에는 파일 선택기, 색상 선택기, 날짜/시간 선택기, 숫자 슬라이더, 텍스트 필드의 추천 옵션, 펼쳐지는 요약/상세, FAQ, 컨트롤이 있는 미디어 플레이어 같은 것들이 있음
    이런 요소들은 가볍고 의미론적이라서 좋고, 같은 name을 공유하는 <details>는 하나를 열면 이전 항목이 닫히는 동작도 가능함

    • 요즘은 결국 브라우저나 버전마다 동작이 충분히 예측 가능하지 않아서, 이런 것들은 툴킷으로 대체되는 경우가 많음
    • 컨트롤이 있는 미디어 플레이어는 브라우저마다 전부 다르게 보이고, JavaScript 없이는 스타일링도 할 수 없음
      더 잘 구현됐으면 좋겠음
  • 2019년부터 <dialog> 를 썼는데, Firefox와 Safari가 몇 년 더 지원하지 않았어도 Google의 polyfill 품질이 좋아서 업무용 SaaS 프로덕션에서 문제 없이 사용했음
    가장 아쉬운 점은 요소가 거의 스타일링되지 않았다는 것임. 아주 기본적인 두꺼운 검은 픽셀 테두리와 각진 모서리뿐이라, 브라우저가 운영체제의 대화상자/창 배치 관례를 따라 네이티브 macOS, iOS, Windows 대화상자처럼 보이게 해주길 기대했던 것과 달랐음
    열린 요소가 별도의 최상위 레이어에 있으니 브라우저 뷰포트 밖으로도 나갈 수 있기를 바랐지만, 그런 기대도 깨졌음. 사용자 입장에서는 문서 내용을 완전히 가리는 움직일 수 없는 팝업이나 모달 대화상자를 원하지 않기 때문임
    브라우저 업체들은 alert(), prompt(), confirm()이 JavaScript/메인 스레드를 막으니 쓰지 않길 바라는 것 같지만, 그만큼 간단하고 효과적이며 UI 디자인 능력을 요구하지 않는 대체 API는 내놓지 못했음. 모든 상황에서 <dialog>가 더 낫다고 설득하려 하기보다, Promise 기반 비차단 alert/prompt/confirm API를 제공하면 좋겠음

    • 브라우저 뷰포트 밖으로 벗어나고 시스템 대화상자처럼 스타일링되는 기능은 사용자나 브라우저 제작자보다 개발자가 더 원할 법한 기능임
      새 기능들이 예전 alert() 계열처럼 동작하지 않게 된 건 우연한 실망이 아니라 장점도 있음
      다만 <dialog> 기본 스타일에는 조금 더 신경 쓸 필요가 있었고, DOM 밖의 시스템 대화상자처럼 100% 보이고 동작하지 않더라도 브라우저 기본 스타일과 어울리는 기본 스타일만 있어도 훨씬 나았을 것임
      PWA처럼 일반 페이지보다 권한이 많은 웹 앱이라면, 창 스타일링이나 시스템과 상호작용하는 별도 기능처럼 이런 방향이 더 잘 받아들여질 수 있음
    • 벤더가 <dialog>를 네이티브 브라우저 창처럼 보이게 만들면 line of death를 흐릴 수 있음
      악성 웹사이트가 페이지 한가운데에 “브라우저 업데이트” 팝업을 띄우고, 그럴듯한 Chrome 다운로드 페이지로 리디렉션해 변조된 실행 파일을 내려받게 만들기 쉬워짐
      https://textslashplain.com/2017/01/14/the-line-of-death/
    • 브라우저 창 전체의 포커스를 막는 모달은 좋은 생각이 아님
      사용자는 탭을 여러 개 열어두고 있고, 다른 탭에 대화상자를 완료하는 데 필요한 정보가 있을 수도 있음
      실제 대화상자에 얼마나 많은 시각적 제어권을 줄지도 매우 조심해야 함. 특히 운영체제처럼 보이게 만들면, 지금도 가짜 브라우저 바이러스 경고에 속는 사람이 많은데 진짜처럼 보이고 반드시 상호작용해야 하는 대화상자는 공격 성공률을 크게 높일 수 있음
    • 거의 스타일링되지 않는다는 점이 내장 브라우저 컴포넌트의 광범위한 사용을 막는 핵심 이유로 보임
      일했던 회사들은 대부분 어느 정도 일관된 디자인 언어가 있었고, 순수 HTML/CSS를 선호하는 사람이 뭐라고 하든 내장 브라우저 컴포넌트를 그대로 쓰면 대부분 아마추어처럼 어색해 보였음
      이 문제가 해결되지 않으면 널리 쓰이기 어려움
    • alert(), prompt(), confirm() 대체품을 await Prompts.alert("This is an alert message!");, await Prompts.confirm(...), await Prompts.prompt(...)처럼 동작하게 만드는 아이디어를 시험 중임
      데모: https://tools.simonwillison.net/prompts-js
      코드는 o1이 작성함: https://chatgpt.com/share/67539c28-4df0-8006-b021-4f468e011f...
  • “The HTML dialog element API is a mess”라는 글을 썼음: https://lapcatsoftware.com/articles/2024/2/1.html

    • 웹 표준이 훌륭하고 잘 설계됐으며 잘 관리된다고 말할 사람은 많지 않을 것임
      그래도 가치는 표준이라는 데 있음. 웹은 어디서나 된다는 점에서 훌륭하고, 지저분할 수밖에 없음
      이 맥락에서 <dialog>는 특히 내부 관리 도구에선 성공이라고 봄. 최신 프런트엔드 유행을 신경 쓰지 않고, 화면 공간을 아끼면서 메인 뷰 위에 콘텐츠를 모달 오버레이로 띄우고 싶을 뿐임
    • 같은 대화상자를 모달로 두 번 여는 일은 현실적으로 거의 없을 것 같음
      대화상자를 열려면 사용자 입력이 필요하고, 모달 대화상자는 사용자 입력을 막으므로 이 일이 발생하려면 대화상자 안의 입력이 다시 그 대화상자를 열어야 함
      비동기 작업으로 대화상자를 연다면 무엇이 열려 있고 닫혀 있는지 추적해야 하며, Qt 같은 환경에서도 비슷한 일이 생김
    • <dialog>가 여러 번 열어도 브라우저마다 경고가 뜨지 않는, 커스터마이즈 가능한 await confirm/prompt의 강화판이 되길 기대했음
      하지만 실제로는 미화된 div에 더 가까움
    • Google Chrome은 URLPattern도 만들었고, Chrome과 Safari가 지원하지 않길 바랐음
      Compression Streams API는 나쁘지 않았지만 아주 작은 API였음
      패턴을 보면 Google은 사용자 경험과 개발자 경험을 잘 못 다루는 것 같음
      덧붙여 표준 입장을 찾아보니 둘 다 URLPattern을 지원함
  • 구현과 무관하게 올바른 방향으로 가는 한 걸음이었다고 봄
    현재 <select>를 강화한 것 같은 제안도 진행 중임: https://open-ui.org/components/combobox.explainer/
    토스트 알림에는 이미 브라우저에 Popover API가 들어와 있음: https://mdn.github.io/dom-examples/popover-api/
    툴팁을 위한 popover hint 제안도 있음: https://open-ui.org/components/popover-hint.research.explain...

  • <dialog> 요소를 좋아하고, 특히 내장되고 표준화된 접근성 고려가 마음에 듦
    Safari 15.4 미만이 지원 기준에서 빠지면 polyfill 없이 쓸 수 있는 날을 기대하고 있음
    다만 가장 큰 불만은 JavaScript 의존성임. 사이트 방문자는 거의 모두 JavaScript를 켜고 오겠지만, 굳이 그럴 필요가 없게 만드는 데서 만족감을 얻기도 함
    왜 CSS나 대상 버튼으로 대화상자의 열린 상태를 제어할 수 없는지 모르겠고, 내가 틀렸다면 알고 싶음

    • 그래서 주로 popover="" 속성을 붙인 커스텀 요소를 씀
      버튼으로 쉽게 대상으로 삼을 수 있고 클라이언트 측 JavaScript가 필요 없으며, 바깥을 클릭하면 닫을 수도 있음. <dialog>에는 기본적으로 없는 동작임
      접근성은 확신할 수 없지만, 숨겨진 레이블/체크박스/폼 요소로 만들던 예전 방식보다는 나쁘지 않을 것 같고 훨씬 단순하며 덜 해킹 같음
    • 이건 invokers로 가능해질 예정임
      https://css-tricks.com/invoker-commands-additional-ways-to-w...
  • HTML이 하나의 파일 안에 여러 페이지를 정의하고 한 번에 하나씩 보여줄 수 있는 <page> 같은 태그 개념을 지원하면 좋겠음
    대화상자처럼 보이지 않으면서, 각 PAGE가 선택된 상태에 따라 같은 페이지 안의 헤더, 사이드바, 푸터 같은 공통 섹션을 끌어올 수 있으면 좋겠음
    지금도 div를 숨기고 보이는 방식으로 할 수는 있지만, 특정 HTML 태그로 이런 접근을 지원하면 도움이 될 수 있음

    • 최근 인쇄용 스타일시트 쪽에는 그런 식으로 할 수 있는 개선이 있었던 것 같지만, 화면 표시용으로는 아직 없는 것으로 앎
  • 오늘 써보다가 우회하지 못한 문제가 있었음
    대화상자 안에 폼이 있으면, 입력에 포커스한 상태에서 Enter를 누르거나 제출 버튼에 포커스한 상태에서 Space를 누를 때 폼 제출과 함께 대화상자가 닫힘
    이를 깔끔하게 막는 방법을 찾지 못했음. 보통 폼은 페이지를 다시 불러오니 일반적인 문제는 아니겠지만, 여기서는 htmx를 쓰고 있었음

    • 마지막 문장이 맞을 가능성이 큼. 기본적으로 폼은 네트워크 요청을 보냄
      <dialog> 안의 폼으로 iframe을 갱신하는 편집기를 쓰고 있는데, 대상 iframe이 다시 로드될 뿐 대화상자는 닫히지 않음
      Enter를 눌렀을 때 대화상자가 닫히는 경우도 재현하지 못했음. submit과 같아야 하고, submit도 대화상자를 닫지 않음
      참고로 버튼은 대화상자를 닫을 수 있다는 걸 어제 배웠음
    • preventDefaultstopPropagation을 쓰면 되지 않나?
    • HTMX 쪽에 버그를 제보하는 게 좋을 수도 있음
  • 새로 최적화한 빌드에 넣을 쿠키 동의 관리자를 찾다가, 오픈소스 옵션들이 100KB가 넘는 게 마음에 들지 않아서 직접 만들었음: https://github.com/replete/biscuitman
    최대한 작게 작성하려고 <dialog> 에 의존했고, CSS 규칙 몇 개만으로 스타일 없이도 네이티브하게 동작함
    결국 IE11과 정말 오래된 브라우저 버전까지 컴파일하는 빌드 도구도 작성하게 됨
    <dialog>는 대체로 잘 동작하고, 오래된 브라우저에서는 CSS 꼼수가 조금 필요하지만 다루기 어렵지 않음. 웹 플랫폼에 괜찮은 추가 요소지만, 20년 동안 이 일을 해보니 몇 년마다 커스텀 다중 선택 컨트롤을 다시 만들고 싶지는 않음. 네이티브 컨트롤이 좋음

  • 대부분 예제의 일반적인 닫기 동작이 Android Firefox에서 동작하지 않음

    • 어제 프로젝트에서 쓸 때는 닫기 버튼의 autofocus 속성이 동작하지 않았음
      결국 show()를 호출할 때마다 $('#thatModal *[autofocus]').focus() 같은 줄을 추가했음
      MDN에 따르면 기본으로 의도대로 동작해야 함
    • “대부분 예제의 일반적인 닫기”가 무엇을 뜻하는지 더 설명해줄 수 있나? 내가 본 예제들은 모두 닫기 버튼에 이벤트 리스너를 붙이는 JavaScript 조각이 있었고, Firefox for Android에서 잘 동작했음
    • Windows 10의 Chrome에서도 같음
      JavaScript로 추가한 리스너만 제대로 동작하는 것 같음