Dialog
A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.
<button type="button" onclick="document.getElementById('demo-dialog-edit-profile').showModal()" class="btn-outline">Edit Profile</button>
<dialog id="demo-dialog-edit-profile" class="dialog" aria-labelledby="demo-dialog-edit-profile-title" aria-describedby="demo-dialog-edit-profile-description" onclick="if (event.target === this) this.close()">
<div>
<header>
<h2 id="demo-dialog-edit-profile-title">Edit profile</h2>
<p id="demo-dialog-edit-profile-description">Make changes to your profile here. Click save when you're done.</p>
</header>
<section>
<form class="grid gap-4">
<div class="grid gap-3">
<label class="label" for="demo-dialog-edit-profile-name">Name</label>
<input class="input" type="text" value="Pedro Duarte" id="demo-dialog-edit-profile-name" autofocus />
</div>
<div class="grid gap-3">
<label class="label" for="demo-dialog-edit-profile-username">Username</label>
<input class="input" type="text" value="@peduarte" id="demo-dialog-edit-profile-username" />
</div>
</form>
</section>
<footer>
<button class="btn-outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" onclick="this.closest('dialog').close()">Save changes</button>
</footer>
<button type="button" class="btn-sm-icon-ghost" aria-label="Close dialog" onclick="this.closest('dialog').close()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x">
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
</dialog>
Usage
Basecoat uses the native <dialog> element and showModal(). This differs from shadcn/ui's portalled Base UI implementation, but preserves native modality, focus handling, and inert page content without component JavaScript.
HTML
<button type="button" onclick="document.getElementById('demo-dialog-edit-profile').showModal()" class="btn-outline">Edit Profile</button>
<dialog id="demo-dialog-edit-profile" class="dialog" aria-labelledby="demo-dialog-edit-profile-title" aria-describedby="demo-dialog-edit-profile-description" onclick="if (event.target === this) this.close()">
<div>
<header>
<h2 id="demo-dialog-edit-profile-title">Edit profile</h2>
<p id="demo-dialog-edit-profile-description">Make changes to your profile here. Click save when you're done.</p>
</header>
<section>
<form class="grid gap-4">
<div class="grid gap-3">
<label class="label" for="demo-dialog-edit-profile-name">Name</label>
<input class="input" type="text" value="Pedro Duarte" id="demo-dialog-edit-profile-name" autofocus />
</div>
<div class="grid gap-3">
<label class="label" for="demo-dialog-edit-profile-username">Username</label>
<input class="input" type="text" value="@peduarte" id="demo-dialog-edit-profile-username" />
</div>
</form>
</section>
<footer>
<button class="btn-outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" onclick="this.closest('dialog').close()">Save changes</button>
</footer>
<button type="button" class="btn-sm-icon-ghost" aria-label="Close dialog" onclick="this.closest('dialog').close()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x">
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
</dialog>
HTML structure
The component has the following HTML structure:
<button type="button" onclick="dialog.showModal()">Optional- Trigger button. Basecoat intentionally uses the native
HTMLDialogElement.showModal()method. <dialog class="dialog" id="{ DIALOG_ID }">- Native modal dialog. Add
aria-labelledby="{ TITLE_ID }"andaria-describedby="{ DESCRIPTION_ID }"when title and description are present. The macro also adds backdrop-click close handling.<div>- Dialog content surface.
<header>Optional- Dialog header.
<h2 id="{ TITLE_ID }">- Dialog title. Reference it from
aria-labelledby. <p id="{ DESCRIPTION_ID }">Optional- Dialog description. Reference it from
aria-describedby.
<section>Optional- Dialog body/content area. Add overflow utilities when the body should scroll.
<footer>Optional- Action area. It stacks actions on small screens and aligns them to the end on larger screens.
<button type="button" onclick="this.closest('dialog').close()">Optional- Close button. You can also wrap a button in
<form method="dialog">.
Jinja and Nunjucks
You can use the dialog() Nunjucks or Jinja macro for this component.
{% set footer %}
<button class="btn-outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" onclick="this.closest('dialog').close()">Save changes</button>
{% endset %}
{% call dialog(
id="dialog-edit-profile",
title="Edit profile",
description="Make changes to your profile here.",
trigger="Edit Profile",
trigger_attrs={"class": "btn-outline"},
footer=footer
) %}
<form class="grid gap-4">...</form>
{% endcall %}
Examples
Custom Close Button
<button type="button" onclick="document.getElementById('dialog-custom-close').showModal()" class="btn-outline">Open</button>
<dialog id="dialog-custom-close" class="dialog" aria-labelledby="dialog-custom-close-title" aria-describedby="dialog-custom-close-description" onclick="if (event.target === this) this.close()">
<div>
<header>
<h2 id="dialog-custom-close-title">Custom close button</h2>
<p id="dialog-custom-close-description">The default close icon is hidden and the footer provides the close action.</p>
</header>
<section>
<p class="text-sm text-muted-foreground">
Use
<code>close_button=false</code>
and add a close control where it fits your layout.
</p>
</section>
<footer>
<button class="btn-outline" onclick="this.closest('dialog').close()">Close</button>
<button class="btn" onclick="this.closest('dialog').close()">Continue</button>
</footer>
</div>
</dialog>
No Close Button
<button type="button" onclick="document.getElementById('dialog-no-close').showModal()" class="btn-outline">Open</button>
<dialog id="dialog-no-close" class="dialog" aria-labelledby="dialog-no-close-title" aria-describedby="dialog-no-close-description" onclick="if (event.target === this) this.close()">
<div>
<header>
<h2 id="dialog-no-close-title">No close button</h2>
<p id="dialog-no-close-description">The dialog can still be closed with Escape or a custom action.</p>
</header>
<section>
<p class="text-sm text-muted-foreground">Native dialogs remain keyboard-accessible even without the default close icon.</p>
</section>
<footer>
<button class="btn" onclick="this.closest('dialog').close()">I understand</button>
</footer>
</div>
</dialog>
Sticky Footer
<button type="button" onclick="document.getElementById('dialog-sticky-footer').showModal()" class="btn-outline">Sticky Footer</button>
<dialog id="dialog-sticky-footer" class="dialog" aria-labelledby="dialog-sticky-footer-title" aria-describedby="dialog-sticky-footer-description" onclick="if (event.target === this) this.close()">
<div>
<header>
<h2 id="dialog-sticky-footer-title">Sticky footer</h2>
<p id="dialog-sticky-footer-description">Keep actions visible while the body scrolls.</p>
</header>
<section class="overflow-y-auto scrollbar">
<div class="space-y-4 text-sm text-muted-foreground">
<p>Dialog content row 1. Add enough content to make the body scroll while the footer remains available.</p>
<p>Dialog content row 2. Add enough content to make the body scroll while the footer remains available.</p>
<p>Dialog content row 3. Add enough content to make the body scroll while the footer remains available.</p>
<p>Dialog content row 4. Add enough content to make the body scroll while the footer remains available.</p>
<p>Dialog content row 5. Add enough content to make the body scroll while the footer remains available.</p>
<p>Dialog content row 6. Add enough content to make the body scroll while the footer remains available.</p>
<p>Dialog content row 7. Add enough content to make the body scroll while the footer remains available.</p>
<p>Dialog content row 8. Add enough content to make the body scroll while the footer remains available.</p>
</div>
</section>
<footer class="sticky bottom-0">
<button class="btn-outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" onclick="this.closest('dialog').close()">Save</button>
</footer>
<button type="button" class="btn-sm-icon-ghost" aria-label="Close dialog" onclick="this.closest('dialog').close()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x">
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
</dialog>
Scrollable Content
<button type="button" onclick="document.getElementById('dialog-scrollable').showModal()" class="btn-outline">Scrollable Content</button>
<dialog id="dialog-scrollable" class="dialog" aria-labelledby="dialog-scrollable-title" aria-describedby="dialog-scrollable-description" onclick="if (event.target === this) this.close()">
<div>
<header>
<h2 id="dialog-scrollable-title">Scrollable Content</h2>
<p id="dialog-scrollable-description">Long content can scroll inside the dialog body.</p>
</header>
<section class="overflow-y-auto scrollbar">
<div class="space-y-4 text-sm text-muted-foreground">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh sit amet augue commodo ultrices. Row 1.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh sit amet augue commodo ultrices. Row 2.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh sit amet augue commodo ultrices. Row 3.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh sit amet augue commodo ultrices. Row 4.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh sit amet augue commodo ultrices. Row 5.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh sit amet augue commodo ultrices. Row 6.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh sit amet augue commodo ultrices. Row 7.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh sit amet augue commodo ultrices. Row 8.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh sit amet augue commodo ultrices. Row 9.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh sit amet augue commodo ultrices. Row 10.</p>
</div>
</section>
<footer>
<button class="btn-outline" onclick="this.closest('dialog').close()">Close</button>
</footer>
<button type="button" class="btn-sm-icon-ghost" aria-label="Close dialog" onclick="this.closest('dialog').close()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x">
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
</dialog>
RTL
Dialog positioning and close button placement use logical properties. Set dir="rtl" on the dialog or a parent element.
<div dir="rtl">
<button type="button" onclick="document.getElementById('dialog-rtl').showModal()" class="btn-outline">فتح</button>
<dialog id="dialog-rtl" class="dialog" aria-labelledby="dialog-rtl-title" aria-describedby="dialog-rtl-description" onclick="if (event.target === this) this.close()">
<div>
<header>
<h2 id="dialog-rtl-title">تعديل الملف الشخصي</h2>
<p id="dialog-rtl-description">قم بتحديث بياناتك ثم احفظ التغييرات.</p>
</header>
<section>
<form class="grid gap-4">
<div class="grid gap-3">
<label class="label" for="dialog-rtl-name">الاسم</label>
<input class="input" id="dialog-rtl-name" value="ليلى" />
</div>
</form>
</section>
<footer>
<button class="btn-outline" onclick="this.closest('dialog').close()">إلغاء</button>
<button class="btn" onclick="this.closest('dialog').close()">حفظ</button>
</footer>
<button type="button" class="btn-sm-icon-ghost" aria-label="Close dialog" onclick="this.closest('dialog').close()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x">
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
</dialog>
</div>
API Reference
.dialog- Native dialog root. It owns the backdrop through
::backdropand the open state through the nativeopenattribute. .dialog > div- Dialog content surface. This maps to shadcn/ui's
DialogContent. .dialog > div > header- Header area. Direct
h2andpchildren are styled as title and description. .dialog > div > section- Body/content area. Add utilities such as
overflow-y-autowhen you need scrolling. .dialog > div > footer- Action area. Use Basecoat button classes for actions.
close_button=false- Macro option to hide the generated close icon.