原生无法自定义样式(尤其safari/旧edge)、不支持html内容(如图标、多行文本、复选框),且难以嵌入复杂交互;需用模拟下拉菜单,通过role属性、aria状态、键盘导航(arrow/enter/escape)及焦点管理实现可访问的动态交互。

为什么直接用 <select></select> 不够用
因为原生 <select></select> 无法自定义样式(尤其在 Safari 和旧版 Edge 中)、不支持 HTML 内容(比如图标、多行文本、复选框),也不方便嵌入复杂交互逻辑。很多项目需要点击展开、带搜索、可键盘导航的“下拉”,这时就得用 <div> 模拟。
<h3>基础结构:用 <code>div 实现可展开/收起的菜单容器
核心是两个层级:div 作为触发按钮(常叫 dropdown-trigger),另一个 div 作为弹出面板(dropdown-menu),默认隐藏,靠 JS 控制显隐。
关键点:
- 触发器需设
role="button"和aria-expanded="false",便于屏幕阅读器识别 - 菜单容器加
role="listbox",每个选项用role="option",并绑定aria-selected - 用
display: none或visibility: hidden隐藏菜单——但前者更推荐,避免占位干扰布局 - 绝对定位要配合
position: relative在触发器父元素上,否则定位偏移
<div class="dropdown">
<div class="dropdown-trigger" role="button" aria-expanded="false">选择城市</div>
<div class="dropdown-menu" role="listbox" style="display: none;">
<div role="option" data-value="bj">北京</div>
<div role="option" data-value="sh">上海</div>
</div>
</div>
点击与焦点逻辑:避免菜单闪退或失焦丢失
常见错误是点击菜单内部选项时,blur 触发导致菜单立刻关闭;或者按 Tab 键跳过整个菜单。
解决方式:
- 监听触发器的
click,切换菜单显隐,并同步更新aria-expanded - 给菜单本身加
click事件监听(而非只监听选项),阻止事件冒泡到文档级关闭逻辑 - 用
focusin+focusout替代blur,判断焦点是否真正离开整个dropdown区域 - 键盘支持必须处理
ArrowDown/ArrowUp切换选项,Enter或Space确认,Escape关闭
样式与可访问性容易被忽略的细节
纯视觉美化只是表层,真正卡住上线的是这些:
- 菜单出现时,需用
z-index确保盖在其他内容之上,但别设得过高(如 9999),避免遮挡模态框 - 选项 hover/focus 状态必须有明确视觉反馈,且满足 WCAG 对比度要求(至少 4.5:1)
- 移动端需处理
touchstart防止延迟,同时避免click重复触发(iOS 上 click 有 300ms 延迟) - 若菜单高度超出视口,得加滚动逻辑,并把焦点滚动到当前高亮项(用
scrollIntoView({ block: 'nearest' }))
最常漏掉的一点:没给菜单加 aria-labelledby 指向触发器,导致屏幕阅读器读不出“这是谁的菜单”。











