Modal Without JavaScript
Since late 2025, every major browser can open a real modal <dialog> from a plain HTML button — with focus handling, a locked background and Esc to close. Plus two alternatives: popovers and the classic :target modal.
- JavaScript
- 0 KB
- Built with
<dialog>,commandfor,popover,:target- Browsers
- Chrome 135+, Firefox 144+, Safari 26.2+ (
:target: all) - Updated
Yes. Give a <button> the attributes commandfor="my-dialog" and command="show-modal", and it opens <dialog id="my-dialog"> as a true modal. A second button with command="close" closes it; Esc works automatically. This is the Invoker Commands API, supported in all major browsers since late 2025.
Live demo: a real modal dialog
Click the button, or Tab to it and press Enter. Notice what the browser does by itself: focus jumps into the dialog, Tab stays inside it, the page behind can't be clicked, and Esc closes it and returns focus to the button. The links go to real tools.
How the HTML modal works
<dialog>is the modalThe browser has a built-in dialog element. Opened as a modal, it sits on top of everything (the “top layer”), so no
z-indexbattles with your header.commandfor+commandopen itcommandfornames the dialog'sid;command="show-modal"says what to do. That replaces the classicdialog.showModal()script.- The browser handles accessibility
Focus moves into the dialog, the rest of the page becomes inert — unclickable and unreachable by Tab — and closing returns focus to the button that opened it.
aria-labelledbynames itPointing at the heading inside, it makes screen readers announce “Three checks before you launch in China, dialog” when it opens.
::backdropdims the pageThe dark layer behind a modal is a pseudo-element you can style — no extra overlay
<div>needed.- No scrolling behind it
html:has(.ui-modal:modal)matches while the modal is open, so one line of CSS stops the page from scrolling underneath.
Copy the HTML and CSS
Exactly the code running in the demo. All class names start with ui-. The dialog can sit anywhere in your HTML — the browser moves it to the top layer when it opens. Keep the margin: auto: browsers centre dialogs with it, and many sites' CSS resets (* { margin: 0 }) silently push the dialog into the top-left corner.
<button class="ui-modal-open" type="button" commandfor="ui-launch" command="show-modal">
Before you launch in China
</button>
<dialog class="ui-modal" id="ui-launch" aria-labelledby="ui-launch-title">
<h2 id="ui-launch-title">Three checks before you launch in China</h2>
<ol>
<li><a href="/tools/cn-market-entry/">Which licences and registrations does your business need?</a></li>
<li><a href="/tools/chinese-font-stack-generator/">Do your fonts load without Google Fonts?</a></li>
<li><a href="/tools/chinese-filler-text/#build-for-baidu">Is your site built so that Baidu can crawl it?</a></li>
</ol>
<div class="ui-modal__actions">
<a class="ui-modal__primary" href="/#promise">Talk to our team</a>
<button type="button" commandfor="ui-launch" command="close">Close</button>
</div>
</dialog>/* Modal dialog — no JavaScript.
A button with commandfor/command opens a real modal <dialog>:
focus moves inside, the page behind is inert, Esc closes it. */
.ui-modal-open,
.ui-modal button,
.ui-modal__primary {
display: inline-flex;
align-items: center;
padding: 0.7rem 1.2rem;
border: 0;
border-radius: 8px;
font: 600 0.9375rem/1.3 system-ui, -apple-system, "Segoe UI", sans-serif;
text-decoration: none;
cursor: pointer;
}
.ui-modal-open,
.ui-modal__primary {
background: #4f46e5;
color: #fff;
}
.ui-modal button {
background: #eef0ff;
color: #1b1f3b;
}
.ui-modal-open:focus-visible,
.ui-modal :focus-visible {
outline: 2px solid #4f46e5;
outline-offset: 3px;
}
/* The dialog */
.ui-modal {
margin: auto; /* centres it — and survives a global "* { margin: 0 }" reset */
width: min(32rem, calc(100% - 2rem));
padding: 1.75rem;
border: 0;
border-radius: 14px;
background: #fff;
color: #1b1f3b;
box-shadow: 0 24px 60px rgb(10 12 25 / 0.35);
font: 400 1rem/1.6 system-ui, -apple-system, "Segoe UI", sans-serif;
}
.ui-modal h2 {
margin: 0 0 0.75rem;
font-size: 1.25rem;
line-height: 1.3;
}
.ui-modal ol {
margin: 0 0 1.5rem;
padding-left: 1.25rem;
}
.ui-modal li {
margin: 0 0 0.5rem;
}
.ui-modal li a {
color: #4f46e5;
}
.ui-modal__actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
/* Dimmed page behind the dialog */
.ui-modal::backdrop {
background: rgb(10 12 25 / 0.55);
}
/* Fade in (where supported) */
.ui-modal[open] {
animation: ui-modal-in 0.2s ease-out;
}
@keyframes ui-modal-in {
from { opacity: 0; transform: translateY(8px) scale(0.98); }
}
/* Stop the page scrolling while the modal is open */
html:has(.ui-modal:modal) {
overflow: hidden;
}Optional: support for older browsers
Browsers from before late 2025 ignore commandfor, so the button does nothing there. If those visitors matter to you, add this small fallback. Current browsers skip it, so the demo above stays JavaScript-free.
/* Optional fallback for browsers released before late 2025
(Chrome < 135, Firefox < 144, Safari < 26.2), which ignore commandfor.
Current browsers skip this entirely. */
if (!('commandForElement' in HTMLButtonElement.prototype)) {
document.querySelectorAll('button[commandfor]').forEach(function (button) {
button.addEventListener('click', function () {
var dialog = document.getElementById(button.getAttribute('commandfor'));
var command = button.getAttribute('command');
if (command === 'show-modal') dialog.showModal();
if (command === 'close') dialog.close();
});
});
}Alternative 1: a popover (not modal)
A popover floats above the page but doesn't lock it — visitors can keep scrolling and clicking. Esc or a click outside closes it. Good for contact details, short notes and menus. Supported in all major browsers since 2024.
Talk to Jademond
<button class="ui-pop-open" type="button" popovertarget="ui-contact">Contact options</button>
<div class="ui-popcard" id="ui-contact" popover>
<p class="ui-popcard__title">Talk to Jademond</p>
<ul>
<li><a href="tel:+862086358051">+86 (20) 8635 8051</a></li>
<li><a href="mailto:hello@jademond.com">hello@jademond.com</a></li>
<li><a href="/#promise">Contact form</a></li>
</ul>
<button type="button" popovertarget="ui-contact" popovertargetaction="hide">Close</button>
</div>/* Popover card — no JavaScript.
Not modal: the page stays usable. Esc or a click outside closes it. */
.ui-pop-open,
.ui-popcard button {
padding: 0.7rem 1.2rem;
border: 0;
border-radius: 8px;
background: #eef0ff;
color: #1b1f3b;
font: 600 0.9375rem/1.3 system-ui, -apple-system, "Segoe UI", sans-serif;
cursor: pointer;
}
.ui-pop-open:focus-visible,
.ui-popcard :focus-visible {
outline: 2px solid #4f46e5;
outline-offset: 3px;
}
.ui-popcard {
margin: auto; /* centred, even with a global margin reset */
width: min(20rem, calc(100% - 2rem));
padding: 1.25rem;
border: 0;
border-radius: 14px;
background: #fff;
color: #1b1f3b;
box-shadow: 0 18px 44px rgb(10 12 25 / 0.3);
font: 400 0.9375rem/1.6 system-ui, -apple-system, "Segoe UI", sans-serif;
}
.ui-popcard__title {
margin: 0 0 0.5rem;
font-weight: 700;
}
.ui-popcard ul {
margin: 0 0 1rem;
padding: 0;
list-style: none;
}
.ui-popcard li {
margin: 0 0 0.35rem;
}
.ui-popcard a {
color: #4f46e5;
}Alternative 2: the :target modal
The oldest trick: a link to #ui-info makes that element match :target, and CSS shows it. It works in every browser ever made, and the open state is in the URL — so you can link straight to an open modal. But the browser doesn't move focus or lock the page, and Esc does nothing.
What “no JavaScript” means here
Every demo in this library works with JavaScript switched off. Scripts are only added on top — for extras like keyboard shortcuts — never for the core function.
Close<a class="ui-target-open" id="ui-target-open" href="#ui-info">What does “no JavaScript” mean?</a>
<div class="ui-target" id="ui-info">
<div class="ui-target__box">
<p class="ui-target__title">What “no JavaScript” means here</p>
<p>Every demo in this library works with JavaScript switched off. Scripts are only added on top — for extras like keyboard shortcuts — never for the core function.</p>
<p><a href="/lab/css-first-ui/">See all patterns</a></p>
<a class="ui-target__close" href="#ui-target-open">Close</a>
</div>
</div>/* Modal with :target — works in every browser.
The open state is the URL (#ui-info), so it can be linked to.
The close link points back to the opening link, not to "#". */
.ui-target-open,
.ui-target__close {
display: inline-block;
padding: 0.7rem 1.2rem;
border-radius: 8px;
background: #eef0ff;
color: #1b1f3b;
font: 600 0.9375rem/1.3 system-ui, -apple-system, "Segoe UI", sans-serif;
text-decoration: none;
}
.ui-target-open {
/* closing scrolls back here: keep it clear of a fixed header */
scroll-margin-top: 30vh;
}
.ui-target-open:focus-visible,
.ui-target :focus-visible {
outline: 2px solid #4f46e5;
outline-offset: 3px;
}
.ui-target {
position: fixed;
inset: 0;
z-index: 1000;
display: grid;
place-items: center;
padding: 1rem;
background: rgb(10 12 25 / 0.55);
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0s 0.2s;
}
.ui-target:target {
opacity: 1;
visibility: visible;
transition-delay: 0s;
}
.ui-target__box {
width: min(30rem, 100%);
padding: 1.5rem;
border-radius: 14px;
background: #fff;
color: #1b1f3b;
font: 400 0.9375rem/1.6 system-ui, -apple-system, "Segoe UI", sans-serif;
}
.ui-target__box p {
margin: 0 0 0.75rem;
}
.ui-target__title {
font-weight: 700;
}
.ui-target__box a:not(.ui-target__close) {
color: #4f46e5;
}The close link points to #ui-target-open, the link that opened the modal. A close link to # — common in tutorials — makes the page jump to the top.
Which one to use? Accessibility compared
| Feature | <dialog> | Popover | :target |
|---|---|---|---|
| Focus moves into it | Yes | On Tab | No |
| Page behind locked | Yes | By design no | No |
| Esc closes | Yes | Yes | No |
| Click outside closes | No | Yes | With an extra link |
| Focus returns on close | Yes | Yes | Via the close link |
| Link to the open state | No | No | Yes |
| Browsers | Late 2025+ | 2024+ | All |
Use <dialog> with commandfor for anything that needs a decision or full attention. Use a popover for small, non-blocking extras. Keep :target for cases where the open state must be linkable, or where very old browsers matter.
Modals and SEO
Content in a closed modal is in the HTML. All three versions keep the modal's text and links in the page source, so search engines can read them. Modals whose content is fetched by a script when they open are invisible to crawlers that don't click — and Baidu and many AI crawlers mostly work with the raw HTML.
Don't hide important content in modals. Search engines read it, but visitors may never open it, and it carries little weight as page content. Product details, prices and key arguments belong on the page.
Avoid pop-ups that open by themselves on mobile. Google has long warned that intrusive interstitials — a newsletter or app banner covering the content as soon as a mobile page loads — can hurt how a page ranks. A modal the visitor opens with a click is a different matter and causes no such problem.
Questions about modals without JavaScript
Can I open a <dialog> without JavaScript?
Yes, with a button that has commandfor="dialog-id" and command="show-modal". Supported in Chrome 135+, Firefox 144+ and Safari 26.2+.
What is the difference between a dialog and a popover?
A modal dialog blocks the page until it is closed. A popover floats above the page but leaves it usable, and closes when the visitor clicks elsewhere.
How do I close a CSS-only modal by clicking outside?
Popovers do this automatically. For :target modals, add a full-size close link behind the box. A modal <dialog> deliberately doesn't close on an outside click — that protects visitors from losing what they typed.
Is content inside a modal indexed by Google?
Yes, if it is in the HTML. Content loaded by a script when the modal opens is not reliably indexed.
Why does my :target modal make the page jump to the top?
Because the close link points to #. Point it to the id of the link that opened the modal instead, as in the demo — and give that link scroll-margin-top so it doesn't land under a fixed header.
This FAQ is built without JavaScript — see the accordion pattern.
Do your pop-ups help or hurt — on Baidu and in Baidu Ads?
We build and review Chinese landing pages: what crawlers can see, what visitors actually click, and what slows a page down. Then we run Baidu Ads and Baidu SEO for Western companies.
CSS-First UI Patterns are written and maintained by Jademond Digital, a Chinese digital marketing agency founded in Guangzhou in 2012. We build and audit websites for Baidu, Google and AI search — which is why we care what works without JavaScript. About us →