Basic Modal
A titled dialog with a focus trap, Escape to close and focus returned to the trigger.
6 个框架中级
Takes the whole viewport on a phone and collapses to a centred card from sm: up.
<!--
One dialog, two shapes. On a phone it takes the whole viewport - a centred
card with 16px of dimmed backdrop around it wastes the only screen there is,
and a long body inside one scrolls in a cramped box instead of the page. From
sm: up it collapses back to a normal centred panel.
The header is sticky and the BODY scrolls, not the panel: the title and the
close button must stay reachable no matter how far down the content the user
is. A fullscreen dialog whose only exit has scrolled off the top is a trap
with extra steps.
-->
<button class="fsmodal-trigger" type="button" data-fsmodal-open>Open settings</button>
<div class="fsmodal" id="fs-modal" hidden>
<div class="fsmodal__backdrop" data-fsmodal-close></div>
<div class="fsmodal__panel" role="dialog" aria-modal="true" aria-labelledby="fs-modal-title">
<header class="fsmodal__head">
<h2 class="fsmodal__title" id="fs-modal-title">Workspace settings</h2>
<button class="fsmodal__close" type="button" aria-label="Close" data-fsmodal-close>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false">
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</button>
</header>
<div class="fsmodal__body">
<p>Long-form settings content goes here. This region is what scrolls.</p>
</div>
<footer class="fsmodal__foot">
<button class="fsmodal__btn" type="button" data-fsmodal-close>Cancel</button>
<button class="fsmodal__btn fsmodal__btn--primary" type="button" data-fsmodal-close>Save changes</button>
</footer>
</div>
</div>
<script>
(function () {
var FOCUSABLE =
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
var root = document.getElementById('fs-modal');
var trigger = document.querySelector('[data-fsmodal-open]');
var panel = root.querySelector('.fsmodal__panel');
var restore = null;
function focusables() {
return Array.prototype.slice.call(panel.querySelectorAll(FOCUSABLE));
}
function open() {
restore = document.activeElement;
root.hidden = false;
document.body.style.overflow = 'hidden';
var first = focusables()[0];
if (first) first.focus();
}
function close() {
root.hidden = true;
document.body.style.overflow = '';
if (restore && typeof restore.focus === 'function') restore.focus();
}
trigger.addEventListener('click', open);
Array.prototype.forEach.call(root.querySelectorAll('[data-fsmodal-close]'), function (el) {
el.addEventListener('click', close);
});
root.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
close();
return;
}
if (event.key !== 'Tab') return;
var items = focusables();
if (!items.length) return;
var first = items[0];
var last = items[items.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
});
})();
</script>| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
open必填 | boolean | - | 组件当前是否显示。 |
title必填 | string | - | 卡片的标题文本。 |
children必填 | ReactNode | - | 在组件内部渲染的内容。 |
dismissLabel | string | 'Close' | 关闭按钮的无障碍名称。 |
onDismiss必填 | () => void | - | 用户关闭通知时触发。 |
className | string | - | 合并到根元素上的额外类名。 |
Every fullscreen trait is the mobile base and `sm:` undoes it - a centred card with 16px of dimmed backdrop around it wastes the only screen a phone has. Two details do the work: `h-[100dvh]` not `100vh`, because `vh` includes mobile Safari's collapsing URL bar and would push the footer under the browser chrome; and `min-h-0` on the scroll region, without which the flex child refuses to shrink and shoves the footer off the bottom instead of scrolling. The header and footer are `flex-none` so the title and the way out stay reachable however far down the body you are - a fullscreen dialog whose only exit has scrolled away is a trap with extra steps.