[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-dialog-element-native-modal":44,"search-suggestions":60,"quiz-article-dialog-element-native-modal":109,"path-membership-dialog-element-native-modal":337},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"019ff19b-f3ba-73aa-9313-c2d3237b0363","dialog-element-native-modal","PRACTICE_QUIZ","Native \u003Cdialog> element","The native \u003Cdialog> element ships focus trapping, Escape-to-close, a backdrop, and correct ARIA semantics without any library. These questions check the difference between show() and showModal(), how to style the backdrop, how returnValue works, what \u003Cform method=\"dialog\"> does, how to detect backdrop clicks, and current browser support.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},8,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,97,101,105],{"slug":62,"name":63,"articles":64},"webdev","Webdev",139,{"slug":66,"name":67,"articles":68},"javascript","Javascript",113,{"slug":70,"name":71,"articles":72},"frontend","Frontend",84,{"slug":74,"name":75,"articles":76},"tutorial","Tutorial",53,{"slug":78,"name":79,"articles":80},"css","Css",42,{"slug":82,"name":83,"articles":84},"performance","Performance",21,{"slug":86,"name":87,"articles":88},"typescript","Typescript",18,{"slug":90,"name":91,"articles":92},"react","React",17,{"slug":94,"name":95,"articles":96},"browser","Browser",13,{"slug":98,"name":99,"articles":100},"node","Node",12,{"slug":102,"name":103,"articles":104},"programming","Programming",10,{"slug":106,"name":107,"articles":108},"accessibility","Accessibility",9,{"id":110,"slug":46,"title":111,"subtitle":52,"excerpt":112,"coverUrl":113,"locale":13,"readingMinutes":114,"publishedAt":115,"viewCount":116,"likeCount":19,"commentCount":19,"author":117,"vertical":122,"topic":123,"tags":126,"_count":132,"playground":134,"body":136,"bodyMd":323,"seo":324,"translationGroupId":327,"series":52,"podcastUrl":52,"verticalId":5,"thread":328,"assessments":330,"translations":333,"quiz":335},"019ff19b-f388-73f8-aa5b-93d73337d6fd","You built your modal with a `\u003Cdiv>` and a focus trap library. The native `\u003Cdialog>` does all of that.","The `\u003Cdialog>` element ships with focus trapping, Escape-to-close, backdrop rendering, and correct ARIA semantics — no library, no boilerplate. It's been Baseline since 2022.","\u002Fmedia\u002Fcovers\u002Fdialog-element-native-modal.png",5,"2026-08-08T07:00:10.810Z",656,{"id":118,"name":119,"username":120,"avatarUrl":52,"headline":121},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":124,"name":125},"html","HTML",[127,129,130,131],{"slug":124,"name":128,"color":52},"Html",{"slug":62,"name":63,"color":52},{"slug":70,"name":71,"color":52},{"slug":66,"name":67,"color":52},{"assessments":133},1,{"slug":46,"title":135},"&lt;dialog&gt; — interactive playground",{"blocks":137,"version":133},[138,142,145,149,154,159,162,166,169,172,175,179,182,185,189,192,195,198,202,205,208,211,215,218,222,225,229,232,236,239,243,247,250,253,256,259,262,265,268,271,274,277,280,283,286,289,292,295,298,305,308,311,314,317],{"id":139,"html":140,"type":141},"b1","\u003Cp>Building a modal from scratch means writing the same boilerplate every time: a \u003Ccode>&lt;div&gt;\u003C\u002Fcode> with \u003Ccode>role=&quot;dialog&quot;\u003C\u002Fcode>, an \u003Ccode>aria-modal\u003C\u002Fcode> attribute, a \u003Ccode>tabindex=&quot;-1&quot;\u003C\u002Fcode> to steal focus, a \u003Ccode>keydown\u003C\u002Fcode> listener to catch Escape, a focus-trap library to keep Tab cycling inside the dialog, and a \u003Ccode>click\u003C\u002Fcode> listener on the backdrop overlay you rendered yourself. It works, but you&#39;re doing the browser&#39;s job.\u003C\u002Fp>","paragraph",{"id":143,"html":144,"type":141},"b2","\u003Cp>The \u003Ccode>&lt;dialog&gt;\u003C\u002Fcode> element exists to take that job back.\u003C\u002Fp>",{"id":146,"html":147,"text":147,"type":148,"level":31},"b3","The baseline API","heading",{"id":150,"code":151,"type":152,"language":124,"highlight":153},"b4","\u003Cdialog id=\"confirm-dialog\">\n  \u003Ch2>Delete item?\u003C\u002Fh2>\n  \u003Cp>This cannot be undone.\u003C\u002Fp>\n  \u003Cbutton id=\"cancel-btn\">Cancel\u003C\u002Fbutton>\n  \u003Cbutton id=\"confirm-btn\">Confirm\u003C\u002Fbutton>\n\u003C\u002Fdialog>\n\n\u003Cbutton id=\"open-btn\">Delete\u003C\u002Fbutton>","code",[],{"id":155,"code":156,"type":152,"language":157,"highlight":158},"b5","const dialog = document.getElementById('confirm-dialog');\nconst openBtn = document.getElementById('open-btn');\nconst cancelBtn = document.getElementById('cancel-btn');\nconst confirmBtn = document.getElementById('confirm-btn');\n\nopenBtn.addEventListener('click', () => dialog.showModal());\ncancelBtn.addEventListener('click', () => dialog.close());\nconfirmBtn.addEventListener('click', () => {\n  deleteItem();\n  dialog.close();\n});","js",[],{"id":160,"html":161,"type":141},"b6","\u003Cp>That&#39;s it. The browser handles focus trapping (Tab stays inside), Escape-to-close, and ARIA role. No library. No \u003Ccode>keydown\u003C\u002Fcode> listener. No backdrop div.\u003C\u002Fp>",{"id":163,"html":164,"text":165,"type":148,"level":31},"b7","\u003Ccode>showModal()\u003C\u002Fcode> vs \u003Ccode>show()\u003C\u002Fcode>","showModal() vs show()",{"id":167,"html":168,"type":141},"b8","\u003Cp>The element has two open methods and they are meaningfully different.\u003C\u002Fp>",{"id":170,"html":171,"type":141},"b9","\u003Cp>\u003Ccode>dialog.show()\u003C\u002Fcode> opens the dialog as a \u003Cstrong>non-modal\u003C\u002Fstrong>: it&#39;s visible, but the rest of the page is still interactive. Useful for inline panels, drawers, or toasts — not for blocking confirmation flows.\u003C\u002Fp>",{"id":173,"html":174,"type":141},"b10","\u003Cp>\u003Ccode>dialog.showModal()\u003C\u002Fcode> opens it as a \u003Cstrong>modal\u003C\u002Fstrong>: the browser places the dialog in the \u003Ca href=\"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FGlossary\u002FTop_layer\">top layer\u003C\u002Fa> — above all other content, including elements with high \u003Ccode>z-index\u003C\u002Fcode> — and blocks interaction with everything beneath. Focus is trapped inside. Escape closes it. This is what you actually want for a modal dialog.\u003C\u002Fp>",{"id":176,"code":177,"type":152,"language":157,"highlight":178},"b11","dialog.show();      \u002F\u002F non-modal — rest of page still interactive\ndialog.showModal(); \u002F\u002F modal — top layer, focus trapped, Escape works",[],{"id":180,"html":181,"text":181,"type":148,"level":31},"b12","Styling the backdrop",{"id":183,"html":184,"type":141},"b13","\u003Cp>When opened with \u003Ccode>showModal()\u003C\u002Fcode>, the browser renders a backdrop behind the dialog and above the rest of the page. You style it with the \u003Ccode>::backdrop\u003C\u002Fcode> pseudo-element:\u003C\u002Fp>",{"id":186,"code":187,"type":152,"language":78,"highlight":188},"b14","dialog::backdrop {\n  background: rgb(0 0 0 \u002F 50%);\n  backdrop-filter: blur(4px);\n}",[],{"id":190,"html":191,"type":141},"b15","\u003Cp>No more \u003Ccode>position: fixed; inset: 0; background: rgba(0,0,0,0.5)\u003C\u002Fcode> divs. The browser-rendered backdrop is always in the right place, covers the right things, and is animated by the dialog&#39;s own open\u002Fclose transitions.\u003C\u002Fp>",{"id":193,"html":194,"text":194,"type":148,"level":31},"b16","Animating open and close",{"id":196,"html":197,"type":141},"b17","\u003Cp>The \u003Ccode>&lt;dialog&gt;\u003C\u002Fcode> element pairs naturally with \u003Ccode>@starting-style\u003C\u002Fcode> for entry animations. For the exit, you need a small JS helper because the dialog&#39;s \u003Ccode>close()\u003C\u002Fcode> method removes the \u003Ccode>open\u003C\u002Fcode> attribute before a CSS exit transition can play:\u003C\u002Fp>",{"id":199,"code":200,"type":152,"language":78,"highlight":201},"b18","dialog {\n  opacity: 0;\n  transform: scale(0.95);\n  transition: opacity 200ms, transform 200ms, display 200ms allow-discrete;\n}\n\ndialog[open] {\n  opacity: 1;\n  transform: scale(1);\n}\n\n@starting-style {\n  dialog[open] {\n    opacity: 0;\n    transform: scale(0.95);\n  }\n}",[],{"id":203,"html":204,"type":141},"b19","\u003Cp>With \u003Ccode>allow-discrete\u003C\u002Fcode> on the \u003Ccode>display\u003C\u002Fcode> transition (Chrome 117+), the browser holds the dialog in the layout for the duration of the exit transition before hiding it. For older targets, a small \u003Ccode>setTimeout\u003C\u002Fcode> before \u003Ccode>dialog.close()\u003C\u002Fcode> is the reliable fallback.\u003C\u002Fp>",{"id":206,"html":207,"text":207,"type":148,"level":31},"b20","Handling backdrop clicks to close",{"id":209,"html":210,"type":141},"b21","\u003Cp>The backdrop is not a separate element you can listen to directly. The reliable pattern uses the dialog&#39;s own \u003Ccode>click\u003C\u002Fcode> event and checks whether the click landed inside the dialog&#39;s bounding box:\u003C\u002Fp>",{"id":212,"code":213,"type":152,"language":157,"highlight":214},"b22","dialog.addEventListener('click', (event) => {\n  const rect = dialog.getBoundingClientRect();\n  const clickedOutside =\n    event.clientX \u003C rect.left ||\n    event.clientX > rect.right ||\n    event.clientY \u003C rect.top ||\n    event.clientY > rect.bottom;\n\n  if (clickedOutside) dialog.close();\n});",[],{"id":216,"html":217,"type":141},"b23","\u003Cp>The reason this works: when you click the backdrop, the \u003Ccode>&lt;dialog&gt;\u003C\u002Fcode> element itself receives the event — the click target is the dialog, not an element inside it. The bounding box check distinguishes backdrop clicks from content clicks cleanly.\u003C\u002Fp>",{"id":219,"html":220,"text":221,"type":148,"level":31},"b24","Return value from \u003Ccode>close()\u003C\u002Fcode>","Return value from close()",{"id":223,"html":224,"type":141},"b25","\u003Cp>\u003Ccode>dialog.close()\u003C\u002Fcode> accepts an optional string argument that becomes the dialog&#39;s \u003Ccode>returnValue\u003C\u002Fcode>. This lets you communicate \u003Cem>why\u003C\u002Fem> the dialog closed without external state:\u003C\u002Fp>",{"id":226,"code":227,"type":152,"language":157,"highlight":228},"b26","confirmBtn.addEventListener('click', () => dialog.close('confirmed'));\ncancelBtn.addEventListener('click', () => dialog.close('cancelled'));\n\ndialog.addEventListener('close', () => {\n  if (dialog.returnValue === 'confirmed') deleteItem();\n});",[],{"id":230,"html":231,"type":141},"b27","\u003Cp>The \u003Ccode>close\u003C\u002Fcode> event fires whenever the dialog closes — via \u003Ccode>close()\u003C\u002Fcode>, Escape, or a \u003Ccode>&lt;form method=&quot;dialog&quot;&gt;\u003C\u002Fcode> submission. Combined with \u003Ccode>returnValue\u003C\u002Fcode>, it gives you a clean interface for confirmation flows without threading state through callbacks.\u003C\u002Fp>",{"id":233,"html":234,"text":235,"type":148,"level":31},"b28","The \u003Ccode>&lt;form method=&quot;dialog&quot;&gt;\u003C\u002Fcode> shortcut","The \u003Cform method=\"dialog\"> shortcut",{"id":237,"html":238,"type":141},"b29","\u003Cp>If you put a \u003Ccode>&lt;form method=&quot;dialog&quot;&gt;\u003C\u002Fcode> inside a \u003Ccode>&lt;dialog&gt;\u003C\u002Fcode>, any submit button closes the dialog automatically and sets \u003Ccode>returnValue\u003C\u002Fcode> to the button&#39;s \u003Ccode>value\u003C\u002Fcode> attribute — no JavaScript needed for the close logic:\u003C\u002Fp>",{"id":240,"code":241,"type":152,"language":124,"highlight":242},"b30","\u003Cdialog id=\"confirm-dialog\">\n  \u003Cform method=\"dialog\">\n    \u003Ch2>Delete item?\u003C\u002Fh2>\n    \u003Cbutton value=\"cancelled\">Cancel\u003C\u002Fbutton>\n    \u003Cbutton value=\"confirmed\">Confirm\u003C\u002Fbutton>\n  \u003C\u002Fform>\n\u003C\u002Fdialog>",[],{"id":244,"code":245,"type":152,"language":157,"highlight":246},"b31","dialog.addEventListener('close', () => {\n  if (dialog.returnValue === 'confirmed') deleteItem();\n});",[],{"id":248,"html":249,"type":141},"b32","\u003Cp>The form doesn&#39;t submit to a server — \u003Ccode>method=&quot;dialog&quot;\u003C\u002Fcode> is a special value that routes the submission back to the dialog element itself. Useful for simple flows; for complex forms with validation, you&#39;ll still handle submission explicitly.\u003C\u002Fp>",{"id":251,"html":252,"text":252,"type":148,"level":31},"b33","Browser support",{"id":254,"html":255,"type":141},"b34","\u003Cp>\u003Ccode>&lt;dialog&gt;\u003C\u002Fcode> is \u003Cstrong>Baseline 2022\u003C\u002Fstrong>: Chrome 98, Firefox 98, Safari 15.4. Global support is above 95%. You do not need a polyfill for production today. The only missing piece in some older browsers is \u003Ccode>::backdrop\u003C\u002Fcode> styling — the functional modal behavior (focus trap, Escape, top layer) is universally supported.\u003C\u002Fp>",{"id":257,"html":258,"type":141},"b35","\u003C!-- playground:start -->",{"id":260,"html":261,"text":261,"type":148,"level":31},"b36","🎮 Try it yourself",{"id":263,"html":264,"type":141},"b37","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fdialog-element-native-modal\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":266,"html":267,"type":141},"b38","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":269,"html":270,"type":141},"b39","\u003C!-- playground:end -->",{"id":272,"html":273,"type":141},"b40","\u003C!-- quiz:start -->",{"id":275,"html":276,"text":276,"type":148,"level":31},"b41","🧠 Test yourself",{"id":278,"html":279,"type":141},"b42","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fdialog-element-native-modal\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":281,"html":282,"type":141},"b43","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":284,"html":285,"type":141},"b44","\u003C!-- quiz:end -->",{"id":287,"html":288,"text":288,"type":148,"level":31},"b45","The takeaway",{"id":290,"html":291,"type":141},"b46","\u003Cp>Search your codebase for \u003Ccode>role=&quot;dialog&quot;\u003C\u002Fcode> and the focus-trap imports that live near it. Each one is a candidate for replacement: \u003Ccode>&lt;dialog&gt;\u003C\u002Fcode> with \u003Ccode>showModal()\u003C\u002Fcode> handles focus trapping, Escape, backdrop, and ARIA semantics — a library bought you those because the platform didn&#39;t provide them. It does now. Start with the simplest confirmation dialog in your UI, replace it with \u003Ccode>&lt;dialog&gt;\u003C\u002Fcode> and \u003Ccode>showModal()\u003C\u002Fcode>, and you&#39;ll notice what disappears: the \u003Ccode>keydown\u003C\u002Fcode> listener, the backdrop div, the z-index war, and the focus-management ceremony. What remains is the logic that was actually yours to write.\u003C\u002Fp>",{"id":293,"html":294,"type":141},"b47","\u003C!-- related:start -->",{"id":296,"html":297,"text":297,"type":148,"level":31},"b48","📚 Read next",{"id":299,"type":300,"items":301,"ordered":18},"b49","list",[302,303,304],"\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Finert-attribute-disable-interaction\">The Modal Was Open. So Was Everything Behind It.\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fpopover-api-native-popovers\">Your dropdown is a div. The browser has a native popover now.\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-focus-visible-keyboard-focus-rings\">Stop Using outline: none. Use :focus-visible Instead.\u003C\u002Fa>",{"id":306,"html":307,"type":141},"b50","\u003C!-- related:end -->",{"id":309,"type":310},"b51","divider",{"id":312,"html":313,"type":141},"b52","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":315,"html":316,"type":141},"b53","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":318,"type":300,"items":319,"ordered":18},"b54",[320,321,322],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","Building a modal from scratch means writing the same boilerplate every time: a `\u003Cdiv>` with `role=\"dialog\"`, an `aria-modal` attribute, a `tabindex=\"-1\"` to steal focus, a `keydown` listener to catch Escape, a focus-trap library to keep Tab cycling inside the dialog, and a `click` listener on the backdrop overlay you rendered yourself. It works, but you're doing the browser's job.\n\nThe `\u003Cdialog>` element exists to take that job back.\n\n## The baseline API\n\n```html\n\u003Cdialog id=\"confirm-dialog\">\n  \u003Ch2>Delete item?\u003C\u002Fh2>\n  \u003Cp>This cannot be undone.\u003C\u002Fp>\n  \u003Cbutton id=\"cancel-btn\">Cancel\u003C\u002Fbutton>\n  \u003Cbutton id=\"confirm-btn\">Confirm\u003C\u002Fbutton>\n\u003C\u002Fdialog>\n\n\u003Cbutton id=\"open-btn\">Delete\u003C\u002Fbutton>\n```\n\n```js\nconst dialog = document.getElementById('confirm-dialog');\nconst openBtn = document.getElementById('open-btn');\nconst cancelBtn = document.getElementById('cancel-btn');\nconst confirmBtn = document.getElementById('confirm-btn');\n\nopenBtn.addEventListener('click', () => dialog.showModal());\ncancelBtn.addEventListener('click', () => dialog.close());\nconfirmBtn.addEventListener('click', () => {\n  deleteItem();\n  dialog.close();\n});\n```\n\nThat's it. The browser handles focus trapping (Tab stays inside), Escape-to-close, and ARIA role. No library. No `keydown` listener. No backdrop div.\n\n## `showModal()` vs `show()`\n\nThe element has two open methods and they are meaningfully different.\n\n`dialog.show()` opens the dialog as a **non-modal**: it's visible, but the rest of the page is still interactive. Useful for inline panels, drawers, or toasts — not for blocking confirmation flows.\n\n`dialog.showModal()` opens it as a **modal**: the browser places the dialog in the [top layer](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FGlossary\u002FTop_layer) — above all other content, including elements with high `z-index` — and blocks interaction with everything beneath. Focus is trapped inside. Escape closes it. This is what you actually want for a modal dialog.\n\n```js\ndialog.show();      \u002F\u002F non-modal — rest of page still interactive\ndialog.showModal(); \u002F\u002F modal — top layer, focus trapped, Escape works\n```\n\n## Styling the backdrop\n\nWhen opened with `showModal()`, the browser renders a backdrop behind the dialog and above the rest of the page. You style it with the `::backdrop` pseudo-element:\n\n```css\ndialog::backdrop {\n  background: rgb(0 0 0 \u002F 50%);\n  backdrop-filter: blur(4px);\n}\n```\n\nNo more `position: fixed; inset: 0; background: rgba(0,0,0,0.5)` divs. The browser-rendered backdrop is always in the right place, covers the right things, and is animated by the dialog's own open\u002Fclose transitions.\n\n## Animating open and close\n\nThe `\u003Cdialog>` element pairs naturally with `@starting-style` for entry animations. For the exit, you need a small JS helper because the dialog's `close()` method removes the `open` attribute before a CSS exit transition can play:\n\n```css\ndialog {\n  opacity: 0;\n  transform: scale(0.95);\n  transition: opacity 200ms, transform 200ms, display 200ms allow-discrete;\n}\n\ndialog[open] {\n  opacity: 1;\n  transform: scale(1);\n}\n\n@starting-style {\n  dialog[open] {\n    opacity: 0;\n    transform: scale(0.95);\n  }\n}\n```\n\nWith `allow-discrete` on the `display` transition (Chrome 117+), the browser holds the dialog in the layout for the duration of the exit transition before hiding it. For older targets, a small `setTimeout` before `dialog.close()` is the reliable fallback.\n\n## Handling backdrop clicks to close\n\nThe backdrop is not a separate element you can listen to directly. The reliable pattern uses the dialog's own `click` event and checks whether the click landed inside the dialog's bounding box:\n\n```js\ndialog.addEventListener('click', (event) => {\n  const rect = dialog.getBoundingClientRect();\n  const clickedOutside =\n    event.clientX \u003C rect.left ||\n    event.clientX > rect.right ||\n    event.clientY \u003C rect.top ||\n    event.clientY > rect.bottom;\n\n  if (clickedOutside) dialog.close();\n});\n```\n\nThe reason this works: when you click the backdrop, the `\u003Cdialog>` element itself receives the event — the click target is the dialog, not an element inside it. The bounding box check distinguishes backdrop clicks from content clicks cleanly.\n\n## Return value from `close()`\n\n`dialog.close()` accepts an optional string argument that becomes the dialog's `returnValue`. This lets you communicate *why* the dialog closed without external state:\n\n```js\nconfirmBtn.addEventListener('click', () => dialog.close('confirmed'));\ncancelBtn.addEventListener('click', () => dialog.close('cancelled'));\n\ndialog.addEventListener('close', () => {\n  if (dialog.returnValue === 'confirmed') deleteItem();\n});\n```\n\nThe `close` event fires whenever the dialog closes — via `close()`, Escape, or a `\u003Cform method=\"dialog\">` submission. Combined with `returnValue`, it gives you a clean interface for confirmation flows without threading state through callbacks.\n\n## The `\u003Cform method=\"dialog\">` shortcut\n\nIf you put a `\u003Cform method=\"dialog\">` inside a `\u003Cdialog>`, any submit button closes the dialog automatically and sets `returnValue` to the button's `value` attribute — no JavaScript needed for the close logic:\n\n```html\n\u003Cdialog id=\"confirm-dialog\">\n  \u003Cform method=\"dialog\">\n    \u003Ch2>Delete item?\u003C\u002Fh2>\n    \u003Cbutton value=\"cancelled\">Cancel\u003C\u002Fbutton>\n    \u003Cbutton value=\"confirmed\">Confirm\u003C\u002Fbutton>\n  \u003C\u002Fform>\n\u003C\u002Fdialog>\n```\n\n```js\ndialog.addEventListener('close', () => {\n  if (dialog.returnValue === 'confirmed') deleteItem();\n});\n```\n\nThe form doesn't submit to a server — `method=\"dialog\"` is a special value that routes the submission back to the dialog element itself. Useful for simple flows; for complex forms with validation, you'll still handle submission explicitly.\n\n## Browser support\n\n`\u003Cdialog>` is **Baseline 2022**: Chrome 98, Firefox 98, Safari 15.4. Global support is above 95%. You do not need a polyfill for production today. The only missing piece in some older browsers is `::backdrop` styling — the functional modal behavior (focus trap, Escape, top layer) is universally supported.\n\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fdialog-element-native-modal\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fdialog-element-native-modal\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n## The takeaway\n\nSearch your codebase for `role=\"dialog\"` and the focus-trap imports that live near it. Each one is a candidate for replacement: `\u003Cdialog>` with `showModal()` handles focus trapping, Escape, backdrop, and ARIA semantics — a library bought you those because the platform didn't provide them. It does now. Start with the simplest confirmation dialog in your UI, replace it with `\u003Cdialog>` and `showModal()`, and you'll notice what disappears: the `keydown` listener, the backdrop div, the z-index war, and the focus-management ceremony. What remains is the logic that was actually yours to write.\n\n\u003C!-- related:start -->\n\n## 📚 Read next\n\n- [The Modal Was Open. So Was Everything Behind It.](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Finert-attribute-disable-interaction)\n- [Your dropdown is a div. The browser has a native popover now.](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fpopover-api-native-popovers)\n- [Stop Using outline: none. Use :focus-visible Instead.](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-focus-visible-keyboard-focus-rings)\n\n\u003C!-- related:end -->\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":325,"canonical":326,"description":112},"You built your modal with a `\u003Cdiv>` and a focus trap library. The nati","https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fdialog-element-native-modal","019ff19b-f388-73f8-aa5b-97670f13d581",{"id":329,"locked":18},"019ff19b-f3a1-7259-882a-24fdc8bb3f32",[331],{"id":45,"slug":46,"title":48,"_count":332},{"questions":51},[334],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":336,"questionCount":51},{"questions":51},[338],{"path":339,"zone":342,"level":343,"next":347},{"slug":340,"title":341,"accentFrom":10,"accentTo":11},"frontend-developer","Frontend Developer",{"slug":124,"title":125,"tier":133},{"id":344,"label":345,"number":133,"of":346},"01a13a8c-af08-7228-a6be-928a0c118889","Native \u003Cdialog>",4,{"id":348,"label":349,"slug":350},"01a13a8c-af08-7228-a6be-96b69ebff266","Exclusive accordions","details-name-exclusive-accordion"]