原生dialog标签实战:表单校验、焦点陷阱、滚动穿透一次说清

2026-08-17 0 369

以前做模态框,第一反应是引入Element Plus或者Vant的Dialog组件。但这次需求很简单:一个登录弹窗,左边一个输入框,右边一个验证码,下面两个按钮。我一看这场景,直接用了原生<dialog>,结果发现比想象中顺手。当然也踩了几个小坑,今天写下来当个记录。

dialog的基础用法比想象爽

你以为要写一堆定位和遮罩?根本不用。dialog标签自带showModal()close()方法,并且默认带了一个::backdrop伪元素,就是半透明遮罩。还自带顶部层级,不会被其他元素覆盖。

最安逸的是:showModal()时会自动锁焦点,按Tab只能在对话框内循环,不会跳到背后的网页。这点以前自己写roll,写出来总是容易漏Tab键。

先来一个最简单的对话框

<dialog id="myDialog">
    <p>这是一个原生对话框</p>
    <button id="closeBtn">关闭</button>
</dialog>

<button id="openBtn">打开对话框</button>

<script>
    const dialog = document.getElementById('myDialog');
    document.getElementById('openBtn').addEventListener('click', () => {
        dialog.showModal();
    });
    document.getElementById('closeBtn').addEventListener('click', () => {
        dialog.close();
    });
</script>

点开按钮,对话框出现在屏幕中央,背景被一个黑色半透明遮罩盖住。按Esc也能关闭,按Tab不会跑出对话框,这是原生自带的。

真正的转折点:form method=”dialog”

原本我以为表单提交要手动处理,后来发现原生dialog里有个神奇用法:form可以带着method="dialog"。这样提交时,表单数据并不会真的发送到服务器,而是会把submitter按钮的value作为返回值传给close(),然后自动关闭对话框。

<dialog open>
    <form method="dialog">
        <button value="cancel">取消</button>
        <button value="ok">确定</button>
    </form>
</dialog>

点击“确定”按钮,对话框的returnValue就是“ok”,然后关闭。这个特性减少了不少代码。

但问题来了:表单里除了提交按钮还有输入框。如果输入框内容校验失败,我们不希望它关闭对话框。那method="dialog"就不太够用了,因为它不管验证,只要有submit事件就关。

我要的是带表单校验的登录框

所以就回到了传统方式:拦截form的submit事件,做校验,再手动决定是否关闭。我的需求如下:

  • 用户名不能为空,且必须包含“@”。
  • 密码至少6位。
  • 校验失败时在对话框内显示错误提示。
  • 校验通过后假装请求接口,再关闭对话框。

我一开始用简单的方式:在dialog里放一个form,监听它的submit事件,然后preventDefault(),再用el.value等原生属性获取输入值。

<dialog id="loginDialog">
    <form id="loginForm">
        <label>邮箱<input type="email" id="email" required></label>
        <label>密码<input type="password" id="password" minlength="6" required></label>
        <div id="error" style="color:red; display:none;">密码至少6位</div>
        <button type="submit">登录</button>
        <button type="button" id="cancelBtn">取消</button>
    </form>
</dialog>

里面用了requiredminlength,但浏览器对表单校验的提示样式很不统一。我干脆把所有校验逻辑放在JS里,可靠。

监听submit事件,不急着关

const dialog = document.getElementById('loginDialog');
const form = document.getElementById('loginForm');
const error = document.getElementById('error');

form.addEventListener('submit', (e) => {
    e.preventDefault();  // 不自动关闭

    const email = document.getElementById('email').value;
    const password = document.getElementById('password').value;

    if (!email.includes('@')) {
        error.textContent = '邮箱格式不正确';
        error.style.display = 'block';
        return;
    }
    if (password.length < 6) {
        error.textContent = '密码至少6位';
        error.style.display = 'block';
        return;
    }

    // 模拟提交请求
    error.style.display = 'none';
    setTimeout(() => {
        dialog.close('success');
    }, 500);
});

这里dialog.close('success')会把返回值设为success,外部可以通过监听close事件拿到这个值。

焦点和初始焦点

虽然showModal()会把焦点困在对话框内,但它默认会把焦点放在第一个可聚焦的元素上。我的第一个可聚焦元素是邮箱输入框,正好是想要的位置。然后按Tab会依次经过密码框、登录按钮、取消按钮。基本符合预期。

但如果你想让某个按钮一开始就获得焦点,可以在showModal()之后手动调用focus()

dialog.addEventListener('close', () => {
    // 关闭后恢复焦点到打开按钮
    document.getElementById('openBtn').focus();
});

这是可访问性里很重要的一条。例如用户用键盘打开弹窗后关闭,焦点应该回到触发元素上。原来没注意到这个问题,后来用Tab键盘操作才发现弹窗关掉后焦点没了,浏览器不知道把焦点放哪,直接掉到了地址栏。

踩坑记录:滚动穿透

这是所有弹窗的老大难。默认情况下,即使弹出模态框,背后的页面依然可以用滚轮滚动。这样体验很糟,背景内容会滚动出视线,模态框还浮在上面。

以前我还得写JS去监听wheel事件阻止传播,但dialog似乎没有内置处理。我的解决方法简单粗暴:当打开对话框时,给body添加overflow: hidden,关闭时移除。但这样会有一个副作用:页面宽度可能会多出滚动条的位置,导致布局偏移。不过对于日常页面问题不大。

function openDialog() {
    dialog.showModal();
    document.body.style.overflow = 'hidden';
}

function closeDialog() {
    dialog.close();
    document.body.style.overflow = '';
}

如果你用了dialog.addEventListener('cancel')(按Esc触发),也要在里面恢复overflow,不然关了弹窗页面还是不能滚。

dialog.addEventListener('cancel', () => {
    document.body.style.overflow = '';
});

更精细的做法是保存当前的scroll位置,恢复到相同的位置,而不是仅仅让overflow复原。但我的场景足够用了。

另一点:方法dialog与表单的诡异行为

如果我在form上没有method="dialog",就不会有自动关闭。但如果你用了method="dialog",同时你又想用JS阻止它关闭,你会发现在submit事件里preventDefault()根本拦不住。我试过了,只要form的method是dialog,不管preventDefault与否,关闭动作已经发生了。因为关闭对话框的动作是在表单提交之前就触发的?反正不建议混合使用。

所以method="dialog"适合那种不需要校验的简单确认框,有校验逻辑就老老实实用普通表单,自己控制关闭。

做一个完整可运行的demo

下面是我去掉样式(为了符合你的要求),只留功能的完整页面代码。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>原生dialog表单示例</title>
</head>
<body>
    <h2>点击登录按钮打开对话框</h2>
    <button id="openBtn">登录</button>

    <dialog id="loginDialog">
        <form id="loginForm">
            <label>邮箱:<input type="email" id="email" placeholder="name@example.com"></label>
            <label>密码:<input type="password" id="password" placeholder="至少6位"></label>
            <div id="error" style="color: red; display: none;"></div>
            <button type="submit">登录</button>
            <button type="button" id="cancelBtn">取消</button>
        </form>
    </dialog>

    <script>
        const dialog = document.getElementById('loginDialog');
        const form = document.getElementById('loginForm');
        const error = document.getElementById('error');
        const openBtn = document.getElementById('openBtn');
        const cancelBtn = document.getElementById('cancelBtn');

        openBtn.addEventListener('click', () => {
            dialog.showModal();
            document.body.style.overflow = 'hidden';
            document.getElementById('email').focus();
        });

        cancelBtn.addEventListener('click', () => {
            dialog.close('cancel');
        });

        dialog.addEventListener('close', () => {
            document.body.style.overflow = '';
            openBtn.focus();
        });

        dialog.addEventListener('cancel', () => {
            document.body.style.overflow = '';
            openBtn.focus();
        });

        form.addEventListener('submit', (e) => {
            e.preventDefault();
            const email = document.getElementById('email').value;
            const password = document.getElementById('password').value;

            if (!email.includes('@')) {
                error.textContent = '邮箱格式不正确';
                error.style.display = 'block';
                return;
            }
            if (password.length < 6) {
                error.textContent = '密码至少6位';
                error.style.display = 'block';
                return;
            }

            error.style.display = 'none';
            // 模拟请求
            setTimeout(() => {
                dialog.close('success');
            }, 300);
        });
    </script>
</body>
</html>

关于取消按钮的细节

我特意把取消按钮设成了type="button",并让它执行dialog.close('cancel')。如果不设置type, 它默认是submit,会触发表单提交,然后走到了登录逻辑里。所以记住:在表单里,按钮一定要写清楚type。

另一个细节:用户按Esc也会触发cancel事件。这个事件默认会把returnValue设为空字符串?实际上按Esc时,浏览器会先触发cancel事件,默认行为是关闭对话框。如果我们在cancel事件里执行了e.preventDefault(),弹窗就不会关闭。但我想关闭,所以没有阻止默认行为,但我同时在里面手动恢复了overflow。其实在close事件里恢复overflow就够了,因为cancel后必然触发close,除非你阻止了默认行为。不过以防万一,两个地方都写了也无妨。

为什么不用第三方库

最大的原因就一个字:轻。一个登录框而已,不需要引整个UI框架。原生dialog已经解决了模态框最难的几个点:焦点管理、遮罩、顶层显示。我需要处理的只有业务逻辑和滚动穿透。虽然滚动穿透的解决方案不算完美,但它的确是小项目快速上手的利器。

如果你要浏览器兼容到IE或者旧版本微信浏览器,那还是老老实实用div模拟。不过今天都快2025年了,正常使用没太大问题。

小结

原生dialog不是万能的,但它解决了我大部分痛点。尤其是焦点陷阱和顶层显示,这两个功能自己写真的烦。我认为在不需要复杂动画、不需要嵌套弹窗的场景下,优先使用原生dialog是一个很好的选择。希望这篇文章能让你少踩一点坑。

原生dialog标签实战:表单校验、焦点陷阱、滚动穿透一次说清
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 html 原生dialog标签实战:表单校验、焦点陷阱、滚动穿透一次说清 https://www.taomawang.com/web/html/2559.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务